Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef170a8ca7 |
@@ -21,3 +21,7 @@ indent_size = 4
|
|||||||
[*.yml]
|
[*.yml]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.xml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|||||||
@@ -45,3 +45,7 @@ APP_SECRET=change_this_to_something_unique
|
|||||||
#================================================================================
|
#================================================================================
|
||||||
# unlikely, that you need to change this one
|
# unlikely, that you need to change this one
|
||||||
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
||||||
|
|
||||||
|
###> symfony/messenger ###
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||||
|
###< symfony/messenger ###
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -53,7 +53,6 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Which PHP version are you using?
|
label: Which PHP version are you using?
|
||||||
options:
|
options:
|
||||||
- "8.5"
|
|
||||||
- "8.4"
|
- "8.4"
|
||||||
- "8.2"
|
- "8.2"
|
||||||
- "8.3"
|
- "8.3"
|
||||||
|
|||||||
2
.github/release-drafter.yml
vendored
2
.github/release-drafter.yml
vendored
@@ -22,7 +22,7 @@ version-resolver:
|
|||||||
- 'translation'
|
- 'translation'
|
||||||
default: patch
|
default: patch
|
||||||
template: |
|
template: |
|
||||||
**Compatible with PHP 8.1 to 8.5**
|
**Compatible with PHP 8.1 to 8.4**
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/testing.yaml
vendored
8
.github/workflows/testing.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
|
||||||
name: Integration (${{ matrix.php }})
|
name: Integration (${{ matrix.php }})
|
||||||
steps:
|
steps:
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
MAILER_URL: null://localhost
|
MAILER_URL: null://localhost
|
||||||
|
|
||||||
- name: Full test-suite
|
- name: Full test-suite
|
||||||
if: matrix.php != '8.5'
|
if: matrix.php != '8.2'
|
||||||
run: vendor/bin/phpunit tests/
|
run: vendor/bin/phpunit tests/
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
MAILER_URL: null://localhost
|
MAILER_URL: null://localhost
|
||||||
|
|
||||||
- name: Full test-suite with coverage
|
- name: Full test-suite with coverage
|
||||||
if: matrix.php == '8.5'
|
if: matrix.php == '8.2'
|
||||||
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
|
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
MAILER_URL: null://localhost
|
MAILER_URL: null://localhost
|
||||||
|
|
||||||
- name: Upload code coverage
|
- name: Upload code coverage
|
||||||
if: matrix.php == '8.5'
|
if: matrix.php == '8.2'
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ and so much more.
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- PHP 8.1.3 minimum with support for 8.2, 8.3, 8.4, 8.5
|
- PHP 8.1.3 minimum (support for PHP 8.2, 8.3, 8.4)
|
||||||
- MariaDB or MySQL
|
- MariaDB or MySQL
|
||||||
- A webserver and subdomain (subdirectory is not supported)
|
- A webserver and subdomain (subdirectory is not supported)
|
||||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
|
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
|
||||||
@@ -44,7 +44,7 @@ and so much more.
|
|||||||
|
|
||||||
There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html).
|
There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html).
|
||||||
|
|
||||||
And if you don't want to host Kimai, you can use the [Cloud version](https://www.kimai.cloud/) of it.
|
And if you don't want to host Kimai, you can use [the Cloud version](https://www.kimai.cloud/) of it.
|
||||||
|
|
||||||
### Updating Kimai
|
### Updating Kimai
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ Perform EACH version specific task between your version and the new one, otherwi
|
|||||||
|
|
||||||
### Developer
|
### Developer
|
||||||
|
|
||||||
Do not use method chaining: all fluent interface, especially in Entities, are no longer supported.
|
|
||||||
|
|
||||||
Removed translations:
|
Removed translations:
|
||||||
- `action.edit`: use `edit` instead
|
- `action.edit`: use `edit` instead
|
||||||
- `my.profile`: use `user_profile` instead
|
- `my.profile`: use `user_profile` instead
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
/**
|
|
||||||
* @deprecated use invoice-pdf instead
|
|
||||||
*/
|
|
||||||
require('./sass/_invoice.scss');
|
require('./sass/_invoice.scss');
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "8.1.*||8.2.*||8.3.*||8.4.*||8.5.*",
|
"php": "8.1.*||8.2.*||8.3.*||8.4.*",
|
||||||
"ext-gd": "*",
|
"ext-gd": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
@@ -74,6 +74,7 @@
|
|||||||
"symfony/translation": "^6.0",
|
"symfony/translation": "^6.0",
|
||||||
"symfony/twig-bundle": "^6.0",
|
"symfony/twig-bundle": "^6.0",
|
||||||
"symfony/validator": "^6.0",
|
"symfony/validator": "^6.0",
|
||||||
|
"symfony/webhook": "^6.0",
|
||||||
"symfony/webpack-encore-bundle": "^2.0",
|
"symfony/webpack-encore-bundle": "^2.0",
|
||||||
"symfony/yaml": "^6.0",
|
"symfony/yaml": "^6.0",
|
||||||
"twig/cssinliner-extra": "^3.0",
|
"twig/cssinliner-extra": "^3.0",
|
||||||
|
|||||||
274
composer.lock
generated
274
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": "4f02c81fc72ab3bd887f6e8cf1361757",
|
"content-hash": "9d1ceee2eb5d9ad5d14add5cd65f49d2",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "azuyalabs/yasumi",
|
"name": "azuyalabs/yasumi",
|
||||||
@@ -775,16 +775,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/doctrine-bundle",
|
"name": "doctrine/doctrine-bundle",
|
||||||
"version": "2.18.2",
|
"version": "2.18.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/doctrine/DoctrineBundle.git",
|
"url": "https://github.com/doctrine/DoctrineBundle.git",
|
||||||
"reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546"
|
"reference": "b769877014de053da0e5cbbb63d0ea2f3b2fea76"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0ff098b29b8b3c68307c8987dcaed7fd829c6546",
|
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/b769877014de053da0e5cbbb63d0ea2f3b2fea76",
|
||||||
"reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546",
|
"reference": "b769877014de053da0e5cbbb63d0ea2f3b2fea76",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -876,7 +876,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/doctrine/DoctrineBundle/issues",
|
"issues": "https://github.com/doctrine/DoctrineBundle/issues",
|
||||||
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.2"
|
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.1"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -892,7 +892,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-20T21:35:32+00:00"
|
"time": "2025-11-05T14:42:10+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/doctrine-migrations-bundle",
|
"name": "doctrine/doctrine-migrations-bundle",
|
||||||
@@ -4138,16 +4138,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpdocumentor/reflection-docblock",
|
"name": "phpdocumentor/reflection-docblock",
|
||||||
"version": "5.6.6",
|
"version": "5.6.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
|
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
|
||||||
"reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8"
|
"reference": "90614c73d3800e187615e2dd236ad0e2a01bf761"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8",
|
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761",
|
||||||
"reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8",
|
"reference": "90614c73d3800e187615e2dd236ad0e2a01bf761",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4157,7 +4157,7 @@
|
|||||||
"phpdocumentor/reflection-common": "^2.2",
|
"phpdocumentor/reflection-common": "^2.2",
|
||||||
"phpdocumentor/type-resolver": "^1.7",
|
"phpdocumentor/type-resolver": "^1.7",
|
||||||
"phpstan/phpdoc-parser": "^1.7|^2.0",
|
"phpstan/phpdoc-parser": "^1.7|^2.0",
|
||||||
"webmozart/assert": "^1.9.1 || ^2"
|
"webmozart/assert": "^1.9.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mockery/mockery": "~1.3.5 || ~1.6.0",
|
"mockery/mockery": "~1.3.5 || ~1.6.0",
|
||||||
@@ -4196,9 +4196,9 @@
|
|||||||
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
|
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
|
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
|
||||||
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6"
|
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5"
|
||||||
},
|
},
|
||||||
"time": "2025-12-22T21:13:58+00:00"
|
"time": "2025-11-27T19:50:05+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpdocumentor/type-resolver",
|
"name": "phpdocumentor/type-resolver",
|
||||||
@@ -7725,6 +7725,97 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-10-24T13:29:09+00:00"
|
"time": "2025-10-24T13:29:09+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/messenger",
|
||||||
|
"version": "v6.4.28",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/messenger.git",
|
||||||
|
"reference": "6a758210a2392c4a326de84a6c8c70e7e18296f9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/messenger/zipball/6a758210a2392c4a326de84a6c8c70e7e18296f9",
|
||||||
|
"reference": "6a758210a2392c4a326de84a6c8c70e7e18296f9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"psr/log": "^1|^2|^3",
|
||||||
|
"symfony/clock": "^6.3|^7.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/console": "<6.3",
|
||||||
|
"symfony/event-dispatcher": "<5.4",
|
||||||
|
"symfony/event-dispatcher-contracts": "<2.5",
|
||||||
|
"symfony/framework-bundle": "<5.4",
|
||||||
|
"symfony/http-kernel": "<5.4",
|
||||||
|
"symfony/serializer": "<5.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"psr/cache": "^1.0|^2.0|^3.0",
|
||||||
|
"symfony/console": "^6.3|^7.0",
|
||||||
|
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/event-dispatcher": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/http-kernel": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/process": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/property-access": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/rate-limiter": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/routing": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/serializer": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/service-contracts": "^2.5|^3",
|
||||||
|
"symfony/stopwatch": "^5.4|^6.0|^7.0",
|
||||||
|
"symfony/validator": "^5.4|^6.0|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Messenger\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Samuel Roze",
|
||||||
|
"email": "samuel.roze@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Helps applications send and receive messages to/from other applications or via message queues",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/messenger/tree/v6.4.28"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-11-06T11:12:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/mime",
|
"name": "symfony/mime",
|
||||||
"version": "v6.4.30",
|
"version": "v6.4.30",
|
||||||
@@ -8595,6 +8686,75 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-07-10T08:14:14+00:00"
|
"time": "2025-07-10T08:14:14+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/remote-event",
|
||||||
|
"version": "v6.4.24",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/remote-event.git",
|
||||||
|
"reference": "920e68c4f09f460609cf39a3540f1aa19da72c21"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/remote-event/zipball/920e68c4f09f460609cf39a3540f1aa19da72c21",
|
||||||
|
"reference": "920e68c4f09f460609cf39a3540f1aa19da72c21",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"symfony/messenger": "^5.4|^6.1|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\RemoteEvent\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Eases handling remote events",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"event"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/remote-event/tree/v6.4.24"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-07-10T08:14:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/routing",
|
"name": "symfony/routing",
|
||||||
"version": "v6.4.30",
|
"version": "v6.4.30",
|
||||||
@@ -10131,6 +10291,78 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-09-11T09:57:09+00:00"
|
"time": "2025-09-11T09:57:09+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/webhook",
|
||||||
|
"version": "v6.4.24",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/webhook.git",
|
||||||
|
"reference": "95d6f437a17224029983f2bed3cfaca09e530a17"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/webhook/zipball/95d6f437a17224029983f2bed3cfaca09e530a17",
|
||||||
|
"reference": "95d6f437a17224029983f2bed3cfaca09e530a17",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"symfony/http-foundation": "^6.3|^7.0",
|
||||||
|
"symfony/http-kernel": "^6.3|^7.0",
|
||||||
|
"symfony/messenger": "^5.4|^6.1|^7.0",
|
||||||
|
"symfony/remote-event": "^6.3|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Webhook\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Eases sending and consuming webhooks",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"webhook"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/webhook/tree/v6.4.24"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-07-10T08:14:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/webpack-encore-bundle",
|
"name": "symfony/webpack-encore-bundle",
|
||||||
"version": "v2.4.0",
|
"version": "v2.4.0",
|
||||||
@@ -12686,16 +12918,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "react/child-process",
|
"name": "react/child-process",
|
||||||
"version": "v0.6.7",
|
"version": "v0.6.6",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/reactphp/child-process.git",
|
"url": "https://github.com/reactphp/child-process.git",
|
||||||
"reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3"
|
"reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3",
|
"url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159",
|
||||||
"reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3",
|
"reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -12749,7 +12981,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/reactphp/child-process/issues",
|
"issues": "https://github.com/reactphp/child-process/issues",
|
||||||
"source": "https://github.com/reactphp/child-process/tree/v0.6.7"
|
"source": "https://github.com/reactphp/child-process/tree/v0.6.6"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -12757,7 +12989,7 @@
|
|||||||
"type": "open_collective"
|
"type": "open_collective"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-23T15:25:20+00:00"
|
"time": "2025-01-01T16:37:48+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "react/dns",
|
"name": "react/dns",
|
||||||
@@ -14544,7 +14776,7 @@
|
|||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "8.1.*||8.2.*||8.3.*||8.4.*||8.5.*",
|
"php": "8.1.*||8.2.*||8.3.*||8.4.*",
|
||||||
"ext-gd": "*",
|
"ext-gd": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
|
|||||||
22
config/packages/messenger.yaml
Normal file
22
config/packages/messenger.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
framework:
|
||||||
|
messenger:
|
||||||
|
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
|
||||||
|
# failure_transport: failed
|
||||||
|
|
||||||
|
transports:
|
||||||
|
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
||||||
|
# async: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||||
|
# failed: 'doctrine://default?queue_name=failed'
|
||||||
|
sync: 'sync://'
|
||||||
|
|
||||||
|
routing:
|
||||||
|
# Route your messages to the transports
|
||||||
|
# 'App\Message\YourMessage': async
|
||||||
|
|
||||||
|
# when@test:
|
||||||
|
# framework:
|
||||||
|
# messenger:
|
||||||
|
# transports:
|
||||||
|
# # replace with your transport name here (e.g., my_transport: 'in-memory://')
|
||||||
|
# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
|
||||||
|
# async: 'in-memory://'
|
||||||
3
config/routes/webhook.yaml
Normal file
3
config/routes/webhook.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#webhook:
|
||||||
|
# resource: '@FrameworkBundle/Resources/config/routing/webhook.xml'
|
||||||
|
# prefix: /webhook
|
||||||
@@ -38,6 +38,13 @@
|
|||||||
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
|
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
|
||||||
-->
|
-->
|
||||||
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
|
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
|
||||||
|
|
||||||
|
<!-- ###+ symfony/messenger ### -->
|
||||||
|
<!-- Choose one of the transports below -->
|
||||||
|
<!-- MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages -->
|
||||||
|
<!-- MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages -->
|
||||||
|
<env name="MESSENGER_TRANSPORT_DSN" value="doctrine://default?auto_setup=0"/>
|
||||||
|
<!-- ###- symfony/messenger ### -->
|
||||||
</php>
|
</php>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Kimai">
|
<testsuite name="Kimai">
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ final class InvoiceController extends BaseApiController
|
|||||||
#[IsGranted('view_invoice')]
|
#[IsGranted('view_invoice')]
|
||||||
#[OA\Response(response: 200, description: 'Returns a collection of invoices', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/InvoiceCollection')))]
|
#[OA\Response(response: 200, description: 'Returns a collection of invoices', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/InvoiceCollection')))]
|
||||||
#[Route(methods: ['GET'], path: '', name: 'get_invoices')]
|
#[Route(methods: ['GET'], path: '', name: 'get_invoices')]
|
||||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only invoices created at or after this date-time will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only invoices created before or at this date-time will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||||
#[Rest\QueryParam(name: 'customers', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of customer IDs to filter, e.g.: customers[]=1&customers[]=2')]
|
#[Rest\QueryParam(name: 'customers', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of customer IDs to filter, e.g.: customers[]=1&customers[]=2')]
|
||||||
#[Rest\QueryParam(name: 'status', map: true, requirements: 'pending|paid|canceled|new', strict: true, nullable: true, default: [], description: 'Invoice status: pending, paid, canceled, new. Default: all')]
|
#[Rest\QueryParam(name: 'status', map: true, requirements: 'pending|paid|canceled|new', strict: true, nullable: true, default: [], description: 'Invoice status: pending, paid, canceled, new. Default: all')]
|
||||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ final class TimesheetController extends BaseApiController
|
|||||||
#[Rest\QueryParam(name: 'tags', map: true, strict: true, nullable: true, default: [], description: 'List of tag names, e.g. tags[]=bar&tags[]=foo')]
|
#[Rest\QueryParam(name: 'tags', map: true, strict: true, nullable: true, default: [], description: 'List of tag names, e.g. tags[]=bar&tags[]=foo')]
|
||||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|begin|end|rate', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)')]
|
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|begin|end|rate', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)')]
|
||||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: DESC)')]
|
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: DESC)')]
|
||||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records started at or after this date-time will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records started at or before this date-time will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||||
#[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
|
#[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
|
||||||
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
|
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
|
||||||
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
|
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
|
||||||
@@ -380,7 +380,7 @@ final class TimesheetController extends BaseApiController
|
|||||||
#[IsGranted('view_own_timesheet')]
|
#[IsGranted('view_own_timesheet')]
|
||||||
#[OA\Response(response: 200, description: 'Returns a collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
|
#[OA\Response(response: 200, description: 'Returns a collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
|
||||||
#[Route(methods: ['GET'], path: '/recent', name: 'recent_timesheet')]
|
#[Route(methods: ['GET'], path: '/recent', name: 'recent_timesheet')]
|
||||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records started at or after this date will be included. Default: today - 1 year (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included. Default: today - 1 year (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries (default: 10)')]
|
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries (default: 10)')]
|
||||||
public function recentAction(ParamFetcherInterface $paramFetcher): Response
|
public function recentAction(ParamFetcherInterface $paramFetcher): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ final class TranslationCommand extends Command
|
|||||||
->addOption('fill-empty', null, InputOption::VALUE_NONE, 'Pre-fills empty translations with the english version')
|
->addOption('fill-empty', null, InputOption::VALUE_NONE, 'Pre-fills empty translations with the english version')
|
||||||
->addOption('delete-empty', null, InputOption::VALUE_NONE, 'Delete all empty keys and files which have no translated key at all')
|
->addOption('delete-empty', null, InputOption::VALUE_NONE, 'Delete all empty keys and files which have no translated key at all')
|
||||||
->addOption('move-resname', null, InputOption::VALUE_REQUIRED, 'Move a resname from one file to another (needs "source" and "target" options)')
|
->addOption('move-resname', null, InputOption::VALUE_REQUIRED, 'Move a resname from one file to another (needs "source" and "target" options)')
|
||||||
->addOption('copy-resname', null, InputOption::VALUE_REQUIRED, 'Copy a resname within one file (needs "source" option)')
|
|
||||||
->addOption('target-resname', null, InputOption::VALUE_REQUIRED, 'Target resname when copying (needs "source" option)')
|
|
||||||
->addOption('move-all', null, InputOption::VALUE_NONE, 'Move all keys from one file to another (needs "source" and "target" options)')
|
->addOption('move-all', null, InputOption::VALUE_NONE, 'Move all keys from one file to another (needs "source" and "target" options)')
|
||||||
->addOption('source', null, InputOption::VALUE_REQUIRED, 'Single source file to use')
|
->addOption('source', null, InputOption::VALUE_REQUIRED, 'Single source file to use')
|
||||||
->addOption('only-core', null, InputOption::VALUE_NONE, 'Do not include plugin and theme directories')
|
->addOption('only-core', null, InputOption::VALUE_NONE, 'Do not include plugin and theme directories')
|
||||||
@@ -152,21 +150,6 @@ final class TranslationCommand extends Command
|
|||||||
return $this->moveResname($io, $moveResname, $sources, $targets);
|
return $this->moveResname($io, $moveResname, $sources, $targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Move resname from source to target
|
|
||||||
// ==========================================================================
|
|
||||||
$copyResname = $input->getOption('copy-resname');
|
|
||||||
$targetResname = $input->getOption('target-resname');
|
|
||||||
if (\is_string($copyResname)) {
|
|
||||||
if (!\is_string($targetResname)) {
|
|
||||||
$io->error('To copy a resname, we need a target-resname');
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->copyResname($io, $copyResname, $targetResname, $sources);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Move all keys from source to target
|
// Move all keys from source to target
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -671,62 +654,6 @@ final class TranslationCommand extends Command
|
|||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string> $sources
|
|
||||||
*/
|
|
||||||
private function copyResname(SymfonyStyle $io, string $resname, string $target, array $sources): int
|
|
||||||
{
|
|
||||||
foreach ($sources as $source) {
|
|
||||||
$tmp = basename($source);
|
|
||||||
$pos = strpos($tmp, '.');
|
|
||||||
if ($pos === false) {
|
|
||||||
$io->error('Unexpected filename: ' . $source);
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceDocument = new \DOMDocument('1.0');
|
|
||||||
$sourceDocument->load($source);
|
|
||||||
|
|
||||||
$copiedNode = false;
|
|
||||||
|
|
||||||
/** @var \DOMElement $element */
|
|
||||||
foreach ($sourceDocument->getElementsByTagName('trans-unit') as $element) {
|
|
||||||
if (!$element->hasAttribute('resname')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = $element->getAttribute('resname');
|
|
||||||
|
|
||||||
if ($key === $resname) {
|
|
||||||
$newElement = clone $element;
|
|
||||||
$newElement->setAttribute('resname', $target);
|
|
||||||
foreach ($newElement->childNodes->getIterator() as $child) {
|
|
||||||
if ($child->nodeName === 'source') {
|
|
||||||
$child->textContent = $target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$newElement->setAttribute('id', $this->generateId($target));
|
|
||||||
|
|
||||||
$newNode = $sourceDocument->importNode($newElement, true);
|
|
||||||
$sourceDocument->documentElement->firstElementChild->firstElementChild->appendChild($newNode); // @phpstan-ignore-line
|
|
||||||
$copiedNode = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($copiedNode) {
|
|
||||||
$xmlDocument = new \DOMDocument('1.0');
|
|
||||||
$xmlDocument->preserveWhiteSpace = false;
|
|
||||||
$xmlDocument->formatOutput = true;
|
|
||||||
$xmlDocument->loadXML($sourceDocument->saveXML()); // @phpstan-ignore-line
|
|
||||||
file_put_contents($source, $xmlDocument->saveXML());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string> $sources
|
* @param array<string> $sources
|
||||||
* @param array<string> $targets
|
* @param array<string> $targets
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ final class CustomerController extends AbstractController
|
|||||||
$table->addColumn('company', ['class' => 'd-none']);
|
$table->addColumn('company', ['class' => 'd-none']);
|
||||||
$table->addColumn('vat_id', ['class' => 'd-none w-min']);
|
$table->addColumn('vat_id', ['class' => 'd-none w-min']);
|
||||||
$table->addColumn('contact', ['class' => 'd-none']);
|
$table->addColumn('contact', ['class' => 'd-none']);
|
||||||
$table->addColumn('city', ['class' => 'd-none']);
|
$table->addColumn('address', ['class' => 'd-none']);
|
||||||
$table->addColumn('country', ['class' => 'd-none w-min']);
|
$table->addColumn('country', ['class' => 'd-none w-min']);
|
||||||
$table->addColumn('currency', ['class' => 'd-none w-min']);
|
$table->addColumn('currency', ['class' => 'd-none w-min']);
|
||||||
$table->addColumn('phone', ['class' => 'd-none']);
|
$table->addColumn('phone', ['class' => 'd-none']);
|
||||||
|
|||||||
@@ -61,15 +61,18 @@ use Twig\Environment;
|
|||||||
final class InvoiceController extends AbstractController
|
final class InvoiceController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly ServiceInvoice $service,
|
||||||
|
private readonly InvoiceTemplateRepository $templateRepository,
|
||||||
|
private readonly InvoiceRepository $invoiceRepository,
|
||||||
private readonly EventDispatcherInterface $dispatcher
|
private readonly EventDispatcherInterface $dispatcher
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/', name: 'invoice', methods: ['GET', 'POST'])]
|
#[Route(path: '/', name: 'invoice', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('create_invoice')]
|
#[IsGranted('create_invoice')]
|
||||||
public function indexAction(Request $request, ServiceInvoice $service, InvoiceTemplateRepository $templateRepository): Response
|
public function indexAction(Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
if (!$templateRepository->hasTemplate()) {
|
if (!$this->templateRepository->hasTemplate()) {
|
||||||
if ($this->isGranted('manage_invoice_template')) {
|
if ($this->isGranted('manage_invoice_template')) {
|
||||||
return $this->redirectToRoute('admin_invoice_template_create');
|
return $this->redirectToRoute('admin_invoice_template_create');
|
||||||
}
|
}
|
||||||
@@ -92,7 +95,7 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
if ($form->isValid() && $query->getTemplate() !== null) {
|
if ($form->isValid() && $query->getTemplate() !== null) {
|
||||||
try {
|
try {
|
||||||
$models = $service->createModels($query);
|
$models = $this->service->createModels($query);
|
||||||
$searched = true;
|
$searched = true;
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
$this->flashUpdateException($ex);
|
$this->flashUpdateException($ex);
|
||||||
@@ -136,8 +139,12 @@ final class InvoiceController extends AbstractController
|
|||||||
#[Route(path: '/preview/{customer}/{token}', name: 'invoice_preview', methods: ['GET'])]
|
#[Route(path: '/preview/{customer}/{token}', name: 'invoice_preview', methods: ['GET'])]
|
||||||
#[IsGranted('create_invoice')]
|
#[IsGranted('create_invoice')]
|
||||||
#[IsGranted('access', 'customer')]
|
#[IsGranted('access', 'customer')]
|
||||||
public function previewAction(Customer $customer, string $token, Request $request, ServiceInvoice $service): Response
|
public function previewAction(Customer $customer, string $token, Request $request): Response
|
||||||
{
|
{
|
||||||
|
if (!$this->templateRepository->hasTemplate()) {
|
||||||
|
return $this->redirectToRoute('invoice');
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('invoice.preview', $token)) {
|
if (!$this->isCsrfTokenValid('invoice.preview', $token)) {
|
||||||
$this->flashError('action.csrf.error');
|
$this->flashError('action.csrf.error');
|
||||||
|
|
||||||
@@ -157,10 +164,10 @@ final class InvoiceController extends AbstractController
|
|||||||
if ($form->isValid()) {
|
if ($form->isValid()) {
|
||||||
try {
|
try {
|
||||||
$query->setCustomers([$customer]);
|
$query->setCustomers([$customer]);
|
||||||
$model = $service->createModel($query);
|
$model = $this->service->createModel($query);
|
||||||
$model->setPreview(true);
|
$model->setPreview(true);
|
||||||
|
|
||||||
return $service->renderInvoice($model, $this->dispatcher, true);
|
return $this->service->renderInvoice($model, $this->dispatcher, true);
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
$this->flashUpdateException($ex);
|
$this->flashUpdateException($ex);
|
||||||
}
|
}
|
||||||
@@ -174,8 +181,12 @@ final class InvoiceController extends AbstractController
|
|||||||
#[Route(path: '/save-invoice/{customer}/{token}', name: 'invoice_create', methods: ['GET'])]
|
#[Route(path: '/save-invoice/{customer}/{token}', name: 'invoice_create', methods: ['GET'])]
|
||||||
#[IsGranted('create_invoice')]
|
#[IsGranted('create_invoice')]
|
||||||
#[IsGranted('access', 'customer')]
|
#[IsGranted('access', 'customer')]
|
||||||
public function createInvoiceAction(Customer $customer, string $token, Request $request, CustomerRepository $customerRepository, ServiceInvoice $service): Response
|
public function createInvoiceAction(Customer $customer, string $token, Request $request, CustomerRepository $customerRepository): Response
|
||||||
{
|
{
|
||||||
|
if (!$this->templateRepository->hasTemplate()) {
|
||||||
|
return $this->redirectToRoute('invoice');
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('invoice.create', $token)) {
|
if (!$this->isCsrfTokenValid('invoice.create', $token)) {
|
||||||
$this->flashError('action.csrf.error');
|
$this->flashError('action.csrf.error');
|
||||||
|
|
||||||
@@ -192,7 +203,7 @@ final class InvoiceController extends AbstractController
|
|||||||
if ($form->isValid()) {
|
if ($form->isValid()) {
|
||||||
try {
|
try {
|
||||||
$query->setCustomers([$customer]);
|
$query->setCustomers([$customer]);
|
||||||
$model = $service->createModel($query);
|
$model = $this->service->createModel($query);
|
||||||
|
|
||||||
// save default template for customer if not yet set
|
// save default template for customer if not yet set
|
||||||
if ($customer->getInvoiceTemplate() === null) {
|
if ($customer->getInvoiceTemplate() === null) {
|
||||||
@@ -200,7 +211,7 @@ final class InvoiceController extends AbstractController
|
|||||||
$customerRepository->saveCustomer($customer);
|
$customerRepository->saveCustomer($customer);
|
||||||
}
|
}
|
||||||
|
|
||||||
$invoice = $service->createInvoice($model, $this->dispatcher);
|
$invoice = $this->service->createInvoice($model, $this->dispatcher);
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
@@ -218,7 +229,7 @@ final class InvoiceController extends AbstractController
|
|||||||
#[Route(path: '/change-status/{id}/{status}/{token}', name: 'admin_invoice_status', methods: ['GET', 'POST'])]
|
#[Route(path: '/change-status/{id}/{status}/{token}', name: 'admin_invoice_status', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('create_invoice')]
|
#[IsGranted('create_invoice')]
|
||||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||||
public function changeStatusAction(Invoice $invoice, string $status, string $token, Request $request, CsrfTokenManagerInterface $csrfTokenManager, ServiceInvoice $service): Response
|
public function changeStatusAction(Invoice $invoice, string $status, string $token, Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
|
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
|
||||||
$this->flashError('action.csrf.error');
|
$this->flashError('action.csrf.error');
|
||||||
@@ -243,7 +254,7 @@ final class InvoiceController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service->changeInvoiceStatus($invoice, $status);
|
$this->service->changeInvoiceStatus($invoice, $status);
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
$this->flashUpdateException($ex);
|
$this->flashUpdateException($ex);
|
||||||
@@ -255,14 +266,14 @@ final class InvoiceController extends AbstractController
|
|||||||
#[Route(path: '/edit/{id}', name: 'admin_invoice_edit', methods: ['GET', 'POST'])]
|
#[Route(path: '/edit/{id}', name: 'admin_invoice_edit', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('create_invoice')]
|
#[IsGranted('create_invoice')]
|
||||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||||
public function editAction(Invoice $invoice, Request $request, InvoiceRepository $invoiceRepository): Response
|
public function editAction(Invoice $invoice, Request $request): Response
|
||||||
{
|
{
|
||||||
$form = $this->createInvoiceEditForm($invoice);
|
$form = $this->createInvoiceEditForm($invoice);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
try {
|
try {
|
||||||
$invoiceRepository->saveInvoice($invoice);
|
$this->invoiceRepository->saveInvoice($invoice);
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
return $this->redirectToRoute('admin_invoice_list');
|
return $this->redirectToRoute('admin_invoice_list');
|
||||||
@@ -281,7 +292,7 @@ final class InvoiceController extends AbstractController
|
|||||||
#[Route(path: '/delete/{id}/{token}', name: 'admin_invoice_delete', methods: ['GET'])]
|
#[Route(path: '/delete/{id}/{token}', name: 'admin_invoice_delete', methods: ['GET'])]
|
||||||
#[IsGranted('delete_invoice')]
|
#[IsGranted('delete_invoice')]
|
||||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||||
public function deleteInvoiceAction(Invoice $invoice, string $token, CsrfTokenManagerInterface $csrfTokenManager, ServiceInvoice $service): Response
|
public function deleteInvoiceAction(Invoice $invoice, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
|
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
|
||||||
$this->flashError('action.csrf.error');
|
$this->flashError('action.csrf.error');
|
||||||
@@ -292,7 +303,7 @@ final class InvoiceController extends AbstractController
|
|||||||
$csrfTokenManager->refreshToken('invoice.status');
|
$csrfTokenManager->refreshToken('invoice.status');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service->deleteInvoice($invoice, $this->dispatcher);
|
$this->service->deleteInvoice($invoice, $this->dispatcher);
|
||||||
$this->flashSuccess('action.delete.success');
|
$this->flashSuccess('action.delete.success');
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
$this->flashDeleteException($ex);
|
$this->flashDeleteException($ex);
|
||||||
@@ -304,9 +315,9 @@ final class InvoiceController extends AbstractController
|
|||||||
#[Route(path: '/download/{id}', name: 'admin_invoice_download', methods: ['GET'])]
|
#[Route(path: '/download/{id}', name: 'admin_invoice_download', methods: ['GET'])]
|
||||||
#[IsGranted('view_invoice')]
|
#[IsGranted('view_invoice')]
|
||||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||||
public function downloadAction(Invoice $invoice, ServiceInvoice $service): Response
|
public function downloadAction(Invoice $invoice): Response
|
||||||
{
|
{
|
||||||
$file = $service->getInvoiceFile($invoice);
|
$file = $this->service->getInvoiceFile($invoice);
|
||||||
|
|
||||||
if (null === $file) {
|
if (null === $file) {
|
||||||
throw $this->createNotFoundException(
|
throw $this->createNotFoundException(
|
||||||
@@ -319,12 +330,12 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/show/{page}', defaults: ['page' => 1], requirements: ['page' => '[1-9]\d*'], name: 'admin_invoice_list', methods: ['GET'])]
|
#[Route(path: '/show/{page}', defaults: ['page' => 1], requirements: ['page' => '[1-9]\d*'], name: 'admin_invoice_list', methods: ['GET'])]
|
||||||
#[IsGranted('view_invoice')]
|
#[IsGranted('view_invoice')]
|
||||||
public function showInvoicesAction(Request $request, int $page, InvoiceRepository $invoiceRepository): Response
|
public function showInvoicesAction(Request $request, int $page): Response
|
||||||
{
|
{
|
||||||
$invoice = null;
|
$invoice = null;
|
||||||
|
|
||||||
if (null !== ($id = $request->query->get('id'))) {
|
if (null !== ($id = $request->query->get('id'))) {
|
||||||
$invoice = $invoiceRepository->find($id);
|
$invoice = $this->invoiceRepository->find($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = new InvoiceArchiveQuery();
|
$query = new InvoiceArchiveQuery();
|
||||||
@@ -336,7 +347,7 @@ final class InvoiceController extends AbstractController
|
|||||||
return $this->redirectToRoute('admin_invoice_list');
|
return $this->redirectToRoute('admin_invoice_list');
|
||||||
}
|
}
|
||||||
|
|
||||||
$entries = $invoiceRepository->getPagerfantaForQuery($query);
|
$entries = $this->invoiceRepository->getPagerfantaForQuery($query);
|
||||||
$metaColumns = $this->findMetaColumns($query);
|
$metaColumns = $this->findMetaColumns($query);
|
||||||
|
|
||||||
$table = new DataTable('invoices', $query);
|
$table = new DataTable('invoices', $query);
|
||||||
@@ -378,7 +389,7 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/export', name: 'invoice_export', methods: ['GET'])]
|
#[Route(path: '/export', name: 'invoice_export', methods: ['GET'])]
|
||||||
#[IsGranted('view_invoice')]
|
#[IsGranted('view_invoice')]
|
||||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter, InvoiceRepository $invoiceRepository, InvoiceTemplateRepository $templateRepository): Response
|
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||||
{
|
{
|
||||||
$query = new InvoiceArchiveQuery();
|
$query = new InvoiceArchiveQuery();
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
@@ -387,7 +398,7 @@ final class InvoiceController extends AbstractController
|
|||||||
$form->setData($query);
|
$form->setData($query);
|
||||||
$form->submit($request->query->all(), false);
|
$form->submit($request->query->all(), false);
|
||||||
|
|
||||||
$entries = $invoiceRepository->getInvoicesForQuery($query);
|
$entries = $this->invoiceRepository->getInvoicesForQuery($query);
|
||||||
|
|
||||||
$spreadsheet = $exporter->export(
|
$spreadsheet = $exporter->export(
|
||||||
Invoice::class,
|
Invoice::class,
|
||||||
@@ -401,12 +412,12 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/template/{page}', requirements: ['page' => '[1-9]\d*'], defaults: ['page' => 1], name: 'admin_invoice_template', methods: ['GET', 'POST'])]
|
#[Route(path: '/template/{page}', requirements: ['page' => '[1-9]\d*'], defaults: ['page' => 1], name: 'admin_invoice_template', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('manage_invoice_template')]
|
#[IsGranted('manage_invoice_template')]
|
||||||
public function listTemplateAction(int $page, InvoiceTemplateRepository $templateRepository): Response
|
public function listTemplateAction(int $page): Response
|
||||||
{
|
{
|
||||||
$query = new BaseQuery();
|
$query = new BaseQuery();
|
||||||
$query->setPage($page);
|
$query->setPage($page);
|
||||||
|
|
||||||
$entries = $templateRepository->getPagerfantaForQuery($query);
|
$entries = $this->templateRepository->getPagerfantaForQuery($query);
|
||||||
|
|
||||||
$table = new DataTable('invoice_template', $query);
|
$table = new DataTable('invoice_template', $query);
|
||||||
$table->setPagination($entries);
|
$table->setPagination($entries);
|
||||||
@@ -438,16 +449,16 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/template/{id}/edit', name: 'admin_invoice_template_edit', methods: ['GET', 'POST'])]
|
#[Route(path: '/template/{id}/edit', name: 'admin_invoice_template_edit', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('manage_invoice_template')]
|
#[IsGranted('manage_invoice_template')]
|
||||||
public function editTemplateAction(InvoiceTemplate $template, Request $request, InvoiceTemplateRepository $templateRepository): Response
|
public function editTemplateAction(InvoiceTemplate $template, Request $request): Response
|
||||||
{
|
{
|
||||||
return $this->renderTemplateForm($template, $request, $templateRepository);
|
return $this->renderTemplateForm($template, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/document_download/{document}', name: 'admin_invoice_document_download', methods: ['GET'])]
|
#[Route(path: '/document_download/{document}', name: 'admin_invoice_document_download', methods: ['GET'])]
|
||||||
#[IsGranted('upload_invoice_template')]
|
#[IsGranted('upload_invoice_template')]
|
||||||
public function downloadDocument(string $document, ServiceInvoice $service): Response
|
public function downloadDocument(string $document, Environment $twig): Response
|
||||||
{
|
{
|
||||||
$event = new InvoiceDocumentsEvent($service->getDocuments(true));
|
$event = new InvoiceDocumentsEvent($this->service->getDocuments(true));
|
||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
|
|
||||||
foreach ($event->getInvoiceDocuments() as $doc) {
|
foreach ($event->getInvoiceDocuments() as $doc) {
|
||||||
@@ -461,7 +472,7 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('upload_invoice_template')]
|
#[IsGranted('upload_invoice_template')]
|
||||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration, ServiceInvoice $service, InvoiceTemplateRepository $templateRepository): Response
|
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration): Response
|
||||||
{
|
{
|
||||||
$dir = $documentRepository->getUploadDirectory();
|
$dir = $documentRepository->getUploadDirectory();
|
||||||
$invoiceDir = $dir;
|
$invoiceDir = $dir;
|
||||||
@@ -473,11 +484,11 @@ final class InvoiceController extends AbstractController
|
|||||||
$invoiceDir = rtrim($invoiceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
$invoiceDir = rtrim($invoiceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
$used = [];
|
$used = [];
|
||||||
foreach ($templateRepository->findAll() as $template) {
|
foreach ($this->templateRepository->findAll() as $template) {
|
||||||
$used[$template->getRenderer()] = $template;
|
$used[$template->getRenderer()] = $template;
|
||||||
}
|
}
|
||||||
|
|
||||||
$event = new InvoiceDocumentsEvent($service->getDocuments(true));
|
$event = new InvoiceDocumentsEvent($this->service->getDocuments(true));
|
||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
|
|
||||||
$documents = [];
|
$documents = [];
|
||||||
@@ -595,7 +606,7 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/document/{id}/delete/{token}', name: 'invoice_document_delete', methods: ['GET', 'POST'])]
|
#[Route(path: '/document/{id}/delete/{token}', name: 'invoice_document_delete', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('manage_invoice_template')]
|
#[IsGranted('manage_invoice_template')]
|
||||||
public function deleteDocument(string $id, string $token, CsrfTokenManagerInterface $csrfTokenManager, InvoiceDocumentRepository $documentRepository, InvoiceTemplateRepository $templateRepository): Response
|
public function deleteDocument(string $id, string $token, CsrfTokenManagerInterface $csrfTokenManager, InvoiceDocumentRepository $documentRepository): Response
|
||||||
{
|
{
|
||||||
$document = $documentRepository->findByName($id);
|
$document = $documentRepository->findByName($id);
|
||||||
if ($document === null) {
|
if ($document === null) {
|
||||||
@@ -618,7 +629,7 @@ final class InvoiceController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($templateRepository->findAll() as $template) {
|
foreach ($this->templateRepository->findAll() as $template) {
|
||||||
if ($template->getRenderer() === $id) {
|
if ($template->getRenderer() === $id) {
|
||||||
$this->flashError('Document is used and cannot be deleted.');
|
$this->flashError('Document is used and cannot be deleted.');
|
||||||
|
|
||||||
@@ -638,19 +649,19 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/template/create/{id}', name: 'admin_invoice_template_copy', methods: ['GET', 'POST'])]
|
#[Route(path: '/template/create/{id}', name: 'admin_invoice_template_copy', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('manage_invoice_template')]
|
#[IsGranted('manage_invoice_template')]
|
||||||
public function copyTemplateAction(Request $request, InvoiceTemplate $copyFrom, InvoiceTemplateRepository $templateRepository): Response
|
public function copyTemplateAction(Request $request, InvoiceTemplate $copyFrom): Response
|
||||||
{
|
{
|
||||||
return $this->createTemplate($request, $templateRepository, $copyFrom);
|
return $this->createTemplate($request, $copyFrom);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/template/create', name: 'admin_invoice_template_create', methods: ['GET', 'POST'])]
|
#[Route(path: '/template/create', name: 'admin_invoice_template_create', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('manage_invoice_template')]
|
#[IsGranted('manage_invoice_template')]
|
||||||
public function createTemplateAction(Request $request, InvoiceTemplateRepository $templateRepository): Response
|
public function createTemplateAction(Request $request): Response
|
||||||
{
|
{
|
||||||
return $this->createTemplate($request, $templateRepository, null);
|
return $this->createTemplate($request, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createTemplate(Request $request, InvoiceTemplateRepository $templateRepository, ?InvoiceTemplate $copyFrom = null): Response
|
private function createTemplate(Request $request, ?InvoiceTemplate $copyFrom = null): Response
|
||||||
{
|
{
|
||||||
$template = new InvoiceTemplate();
|
$template = new InvoiceTemplate();
|
||||||
$template->setLanguage($request->getLocale());
|
$template->setLanguage($request->getLocale());
|
||||||
@@ -660,12 +671,12 @@ final class InvoiceController extends AbstractController
|
|||||||
$template->setName($copyFrom->getName() . ' (1)');
|
$template->setName($copyFrom->getName() . ' (1)');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->renderTemplateForm($template, $request, $templateRepository);
|
return $this->renderTemplateForm($template, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/template/{id}/delete/{csrfToken}', name: 'admin_invoice_template_delete', methods: ['GET', 'POST'])]
|
#[Route(path: '/template/{id}/delete/{csrfToken}', name: 'admin_invoice_template_delete', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('manage_invoice_template')]
|
#[IsGranted('manage_invoice_template')]
|
||||||
public function deleteTemplate(InvoiceTemplate $template, string $csrfToken, CsrfTokenManagerInterface $csrfTokenManager, InvoiceTemplateRepository $templateRepository): Response
|
public function deleteTemplate(InvoiceTemplate $template, string $csrfToken, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||||
{
|
{
|
||||||
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete_template', $csrfToken))) {
|
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete_template', $csrfToken))) {
|
||||||
$this->flashError('action.csrf.error');
|
$this->flashError('action.csrf.error');
|
||||||
@@ -676,7 +687,7 @@ final class InvoiceController extends AbstractController
|
|||||||
$csrfTokenManager->refreshToken('invoice.delete_template');
|
$csrfTokenManager->refreshToken('invoice.delete_template');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$templateRepository->removeTemplate($template);
|
$this->templateRepository->removeTemplate($template);
|
||||||
$this->flashSuccess('action.delete.success');
|
$this->flashSuccess('action.delete.success');
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
$this->flashDeleteException($ex);
|
$this->flashDeleteException($ex);
|
||||||
@@ -716,7 +727,7 @@ final class InvoiceController extends AbstractController
|
|||||||
$this->flashError('action.update.error', $err);
|
$this->flashError('action.update.error', $err);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderTemplateForm(InvoiceTemplate $template, Request $request, InvoiceTemplateRepository $templateRepository): Response
|
private function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
||||||
{
|
{
|
||||||
$event = new InvoiceTemplateMetaDefinitionEvent($template);
|
$event = new InvoiceTemplateMetaDefinitionEvent($template);
|
||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
@@ -727,7 +738,7 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||||
try {
|
try {
|
||||||
$templateRepository->saveTemplate($template);
|
$this->templateRepository->saveTemplate($template);
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
return $this->redirectToRoute('admin_invoice_template');
|
return $this->redirectToRoute('admin_invoice_template');
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
|
|||||||
#[Assert\Length(max: 50)]
|
#[Assert\Length(max: 50)]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Customer_Entity'])]
|
#[Serializer\Groups(['Customer_Entity'])]
|
||||||
#[Exporter\Expose(label: 'buyerReference')]
|
#[Exporter\Expose(label: 'buyer_reference')]
|
||||||
private ?string $buyerReference = null;
|
private ?string $buyerReference = null;
|
||||||
|
|
||||||
public function __construct(string $name)
|
public function __construct(string $name)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ namespace App\Entity;
|
|||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method array<Tag> getTags()
|
* @method getTags() array
|
||||||
* @method int getBreak()
|
* @method getBreak() int
|
||||||
*/
|
*/
|
||||||
interface ExportableItem
|
interface ExportableItem
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,10 +88,6 @@ class InvoiceTemplate implements EntityWithMetaFields
|
|||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'template', targetEntity: InvoiceTemplateMeta::class, cascade: ['persist'])]
|
#[ORM\OneToMany(mappedBy: 'template', targetEntity: InvoiceTemplateMeta::class, cascade: ['persist'])]
|
||||||
private Collection $meta;
|
private Collection $meta;
|
||||||
/**
|
|
||||||
* @var array<Tax>
|
|
||||||
*/
|
|
||||||
private array $taxRates = [];
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -272,29 +268,16 @@ class InvoiceTemplate implements EntityWithMetaFields
|
|||||||
$this->language = $language;
|
$this->language = $language;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<Tax> $taxRates
|
|
||||||
*/
|
|
||||||
public function setTaxRates(array $taxRates): void
|
|
||||||
{
|
|
||||||
$this->taxRates = $taxRates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Tax[]
|
* @return Tax[]
|
||||||
*/
|
*/
|
||||||
public function getTaxRates(): array
|
public function getTaxRates(): array
|
||||||
{
|
{
|
||||||
if (\count($this->taxRates) > 0) {
|
// TODO make me configurable via UI
|
||||||
return $this->taxRates;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tax = new Tax(
|
$tax = new Tax(
|
||||||
TaxType::STANDARD,
|
TaxType::STANDARD,
|
||||||
$this->vat ?? 0.00,
|
'VAT',
|
||||||
'vat',
|
$this->vat ?? 0.00
|
||||||
true,
|
|
||||||
null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return [$tax];
|
return [$tax];
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use Symfony\Component\Validator\Constraint;
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
/**
|
|
||||||
* @method null|string getSection()
|
|
||||||
* @method void setSection(?string $name)
|
|
||||||
*/
|
|
||||||
interface MetaTableTypeInterface
|
interface MetaTableTypeInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -47,8 +43,6 @@ interface MetaTableTypeInterface
|
|||||||
/**
|
/**
|
||||||
* This will merge the current object with the values from the given $meta instance.
|
* This will merge the current object with the values from the given $meta instance.
|
||||||
* It should NOT update the name or value, but only the form settings.
|
* It should NOT update the name or value, but only the form settings.
|
||||||
*
|
|
||||||
* Settings from the given $meta object will win over the current ones.
|
|
||||||
*/
|
*/
|
||||||
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface;
|
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface;
|
||||||
|
|
||||||
@@ -138,16 +132,6 @@ interface MetaTableTypeInterface
|
|||||||
*/
|
*/
|
||||||
public function getOrder(): int;
|
public function getOrder(): int;
|
||||||
|
|
||||||
/**
|
|
||||||
* FIXME activate with 3.0
|
|
||||||
*/
|
|
||||||
//public function setSection(?string $section): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FIXME activate with 3.0
|
|
||||||
*/
|
|
||||||
//public function getSection(): ?string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether true if this field is defined by a plugin, or false if it is a value stored in the database.
|
* Whether true if this field is defined by a plugin, or false if it is a value stored in the database.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ trait MetaTableTypeTrait
|
|||||||
*/
|
*/
|
||||||
private mixed $data = null;
|
private mixed $data = null;
|
||||||
private bool $updated = false;
|
private bool $updated = false;
|
||||||
private ?string $section = null;
|
|
||||||
|
|
||||||
public function getName(): ?string
|
public function getName(): ?string
|
||||||
{
|
{
|
||||||
@@ -194,11 +193,13 @@ trait MetaTableTypeTrait
|
|||||||
|
|
||||||
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface
|
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface
|
||||||
{
|
{
|
||||||
$this->setConstraints($meta->getConstraints());
|
$this
|
||||||
$this->setIsRequired($meta->isRequired());
|
->setConstraints($meta->getConstraints())
|
||||||
$this->setIsVisible($meta->isVisible());
|
->setIsRequired($meta->isRequired())
|
||||||
$this->setOptions($meta->getOptions());
|
->setIsVisible($meta->isVisible())
|
||||||
$this->setOrder($meta->getOrder());
|
->setOptions($meta->getOptions())
|
||||||
|
->setOrder($meta->getOrder())
|
||||||
|
;
|
||||||
|
|
||||||
if ($meta->getLabel() !== null) {
|
if ($meta->getLabel() !== null) {
|
||||||
$this->setLabel($meta->getLabel());
|
$this->setLabel($meta->getLabel());
|
||||||
@@ -208,10 +209,6 @@ trait MetaTableTypeTrait
|
|||||||
$this->setType($meta->getType());
|
$this->setType($meta->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($meta->getSection() !== null) {
|
|
||||||
$this->setSection($meta->getSection());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,16 +266,6 @@ trait MetaTableTypeTrait
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setSection(?string $section): void
|
|
||||||
{
|
|
||||||
$this->section = $section;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSection(): ?string
|
|
||||||
{
|
|
||||||
return $this->section;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this field is defined by a plugin or just a value stored in the database.
|
* Whether this field is defined by a plugin or just a value stored in the database.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ final class Tax
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TaxType $type,
|
private readonly TaxType $type,
|
||||||
private readonly float $rate,
|
private readonly string $name = 'VAT',
|
||||||
private readonly string $name,
|
private readonly float $rate = 0.0,
|
||||||
private readonly bool $show,
|
|
||||||
private readonly ?string $note,
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -35,14 +33,4 @@ final class Tax
|
|||||||
{
|
{
|
||||||
return $this->rate;
|
return $this->rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isShow(): bool
|
|
||||||
{
|
|
||||||
return $this->rate > 0.0 || $this->show;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getNote(): ?string
|
|
||||||
{
|
|
||||||
return $this->note;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/Entity/WebhookConfiguration.php
Normal file
60
src/Entity/WebhookConfiguration.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
class WebhookConfiguration
|
||||||
|
{
|
||||||
|
private string $name;
|
||||||
|
private string $url;
|
||||||
|
private string $transport;
|
||||||
|
private string $secret;
|
||||||
|
private string $authentication;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
string $url,
|
||||||
|
string $transport,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $secret,
|
||||||
|
string $authentication
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->url = $url;
|
||||||
|
$this->transport = $transport;
|
||||||
|
$this->secret = $secret;
|
||||||
|
$this->authentication = $authentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUrl(): string
|
||||||
|
{
|
||||||
|
return $this->url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTransport(): string
|
||||||
|
{
|
||||||
|
return $this->transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSecret(): string
|
||||||
|
{
|
||||||
|
return $this->secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthentication(): string
|
||||||
|
{
|
||||||
|
return $this->authentication;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/Entity/WebhookEvent.php
Normal file
32
src/Entity/WebhookEvent.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
class WebhookEvent
|
||||||
|
{
|
||||||
|
private string $name;
|
||||||
|
private WebhookConfiguration $configuration;
|
||||||
|
|
||||||
|
public function __construct(string $name, WebhookConfiguration $configuration)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfiguration(): WebhookConfiguration
|
||||||
|
{
|
||||||
|
return $this->configuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,14 +14,6 @@ use App\Invoice\RendererInterface;
|
|||||||
use App\Model\InvoiceDocument;
|
use App\Model\InvoiceDocument;
|
||||||
use Symfony\Contracts\EventDispatcher\Event;
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggered right before an invoice is rendered.
|
|
||||||
*
|
|
||||||
* You can use this event to:
|
|
||||||
* - add invoice hydrator
|
|
||||||
* - read and change invoice model
|
|
||||||
* - change tax rates
|
|
||||||
*/
|
|
||||||
final class InvoicePreRenderEvent extends Event
|
final class InvoicePreRenderEvent extends Event
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ use App\Repository\Query\TimesheetQuery;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FIXME change interface for 3.0
|
* TODO change interface for 3.0
|
||||||
* @method string getType()
|
* @method getType() string
|
||||||
* @method bool isInternal()
|
* @method isInternal() bool
|
||||||
*/
|
*/
|
||||||
interface ExportRendererInterface
|
interface ExportRendererInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
|||||||
$subtotal = $calculator->getSubtotal();
|
$subtotal = $calculator->getSubtotal();
|
||||||
$formatter = $model->getFormatter();
|
$formatter = $model->getFormatter();
|
||||||
$language = $template->getLanguage();
|
$language = $template->getLanguage();
|
||||||
if ($language === null) {
|
|
||||||
throw new \InvalidArgumentException('InvoiceTemplate needs a language');
|
|
||||||
}
|
|
||||||
|
|
||||||
$taxRows = $calculator->getTaxRows();
|
$taxRows = $calculator->getTaxRows();
|
||||||
|
|
||||||
$vat = 0.00;
|
$vat = 0.00;
|
||||||
@@ -52,7 +48,7 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
|||||||
'invoice.language' => $language, // since 1.9
|
'invoice.language' => $language, // since 1.9
|
||||||
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
|
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
|
||||||
'invoice.vat' => $vat, // @deprecated, use invoice.tax_rows instead
|
'invoice.vat' => $vat, // @deprecated, use invoice.tax_rows instead
|
||||||
'invoice.tax_hide' => $model->isHideZeroTax() && $tax === 0.00, // @deprecated, use invoice.tax_rows instead
|
'invoice.tax_hide' => $model->isHideZeroTax() && $tax === 0.00,
|
||||||
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency), // @deprecated, use invoice.tax_rows instead
|
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency), // @deprecated, use invoice.tax_rows instead
|
||||||
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false), // @deprecated, use invoice.tax_rows instead
|
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false), // @deprecated, use invoice.tax_rows instead
|
||||||
'invoice.tax_plain' => $tax, // @deprecated, use invoice.tax_rows instead
|
'invoice.tax_plain' => $tax, // @deprecated, use invoice.tax_rows instead
|
||||||
@@ -95,17 +91,11 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
|||||||
];
|
];
|
||||||
|
|
||||||
$values['invoice.tax_rows'] = [];
|
$values['invoice.tax_rows'] = [];
|
||||||
$counter = 1;
|
|
||||||
foreach ($taxRows as $taxRow) {
|
foreach ($taxRows as $taxRow) {
|
||||||
$tax = $taxRow->getTax();
|
|
||||||
$values['invoice.tax_rows'][] = [
|
$values['invoice.tax_rows'][] = [
|
||||||
'counter' => $counter++,
|
'type' => $taxRow->getTax()->getType()->value,
|
||||||
'type' => $tax->getType()->value,
|
'name' => $taxRow->getTax()->getName(),
|
||||||
'name' => $tax->getName(),
|
'rate' => $taxRow->getTax()->getRate(),
|
||||||
'rate' => $tax->getRate(),
|
|
||||||
'note' => $tax->getNote(),
|
|
||||||
'show' => $tax->isShow(),
|
|
||||||
'currency' => $currency,
|
|
||||||
'amount' => $taxRow->getAmount(), // do not format, only available in twig anyway
|
'amount' => $taxRow->getAmount(), // do not format, only available in twig anyway
|
||||||
'base' => $taxRow->getBasePrice(), // do not format, only available in twig anyway
|
'base' => $taxRow->getBasePrice(), // do not format, only available in twig anyway
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,9 +9,6 @@
|
|||||||
|
|
||||||
namespace App\Invoice;
|
namespace App\Invoice;
|
||||||
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
|
||||||
|
|
||||||
#[AutoconfigureTag]
|
|
||||||
interface InvoiceItemHydrator
|
interface InvoiceItemHydrator
|
||||||
{
|
{
|
||||||
public function setInvoiceModel(InvoiceModel $model): void;
|
public function setInvoiceModel(InvoiceModel $model): void;
|
||||||
|
|||||||
@@ -9,10 +9,20 @@
|
|||||||
|
|
||||||
namespace App\Invoice;
|
namespace App\Invoice;
|
||||||
|
|
||||||
|
use App\Activity\ActivityStatisticService;
|
||||||
|
use App\Customer\CustomerStatisticService;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\ExportableItem;
|
use App\Entity\ExportableItem;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Invoice\Hydrator\InvoiceItemDefaultHydrator;
|
||||||
|
use App\Invoice\Hydrator\InvoiceModelActivityHydrator;
|
||||||
|
use App\Invoice\Hydrator\InvoiceModelCustomerHydrator;
|
||||||
|
use App\Invoice\Hydrator\InvoiceModelDefaultHydrator;
|
||||||
|
use App\Invoice\Hydrator\InvoiceModelIssuerHydrator;
|
||||||
|
use App\Invoice\Hydrator\InvoiceModelProjectHydrator;
|
||||||
|
use App\Invoice\Hydrator\InvoiceModelUserHydrator;
|
||||||
|
use App\Project\ProjectStatisticService;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||||
@@ -33,6 +43,7 @@ final class InvoiceModel
|
|||||||
private ?NumberGeneratorInterface $generator = null;
|
private ?NumberGeneratorInterface $generator = null;
|
||||||
private \DateTimeInterface $invoiceDate;
|
private \DateTimeInterface $invoiceDate;
|
||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
|
private InvoiceFormatter $formatter;
|
||||||
/**
|
/**
|
||||||
* @var InvoiceModelHydrator[]
|
* @var InvoiceModelHydrator[]
|
||||||
*/
|
*/
|
||||||
@@ -53,13 +64,24 @@ final class InvoiceModel
|
|||||||
* @internal use InvoiceModelFactory
|
* @internal use InvoiceModelFactory
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private InvoiceFormatter $formatter,
|
InvoiceFormatter $formatter,
|
||||||
|
CustomerStatisticService $customerStatistic,
|
||||||
|
ProjectStatisticService $projectStatistic,
|
||||||
|
ActivityStatisticService $activityStatistic,
|
||||||
private readonly Customer $customer,
|
private readonly Customer $customer,
|
||||||
private readonly InvoiceTemplate $template,
|
private readonly InvoiceTemplate $template,
|
||||||
private readonly RateCalculatorMode $rateCalculatorMode
|
private readonly RateCalculatorMode $rateCalculatorMode
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
$this->invoiceDate = new \DateTimeImmutable();
|
$this->invoiceDate = new \DateTimeImmutable();
|
||||||
|
$this->formatter = $formatter;
|
||||||
|
$this->addModelHydrator(new InvoiceModelDefaultHydrator());
|
||||||
|
$this->addModelHydrator(new InvoiceModelCustomerHydrator($customerStatistic));
|
||||||
|
$this->addModelHydrator(new InvoiceModelIssuerHydrator());
|
||||||
|
$this->addModelHydrator(new InvoiceModelProjectHydrator($projectStatistic));
|
||||||
|
$this->addModelHydrator(new InvoiceModelActivityHydrator($activityStatistic));
|
||||||
|
$this->addModelHydrator(new InvoiceModelUserHydrator());
|
||||||
|
$this->addItemHydrator(new InvoiceItemDefaultHydrator());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,36 +9,27 @@
|
|||||||
|
|
||||||
namespace App\Invoice;
|
namespace App\Invoice;
|
||||||
|
|
||||||
|
use App\Activity\ActivityStatisticService;
|
||||||
|
use App\Customer\CustomerStatisticService;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
|
use App\Project\ProjectStatisticService;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
|
|
||||||
|
|
||||||
final class InvoiceModelFactory
|
final class InvoiceModelFactory
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @param iterable<InvoiceModelHydrator> $modelHydrators
|
|
||||||
* @param iterable<InvoiceItemHydrator> $itemHydrators
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly RateCalculatorMode $rateCalculatorMode,
|
private readonly CustomerStatisticService $customerStatisticService,
|
||||||
#[TaggedIterator(InvoiceModelHydrator::class)]
|
private readonly ProjectStatisticService $projectStatisticService,
|
||||||
private readonly iterable $modelHydrators,
|
private readonly ActivityStatisticService $activityStatisticService,
|
||||||
#[TaggedIterator(InvoiceItemHydrator::class)]
|
private readonly RateCalculatorMode $rateCalculatorMode
|
||||||
private readonly iterable $itemHydrators,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createModel(InvoiceFormatter $formatter, Customer $customer, InvoiceTemplate $template, InvoiceQuery $query): InvoiceModel
|
public function createModel(InvoiceFormatter $formatter, Customer $customer, InvoiceTemplate $template, InvoiceQuery $query): InvoiceModel
|
||||||
{
|
{
|
||||||
$model = new InvoiceModel($formatter, $customer, $template, $this->rateCalculatorMode);
|
$model = new InvoiceModel($formatter, $this->customerStatisticService, $this->projectStatisticService, $this->activityStatisticService, $customer, $template, $this->rateCalculatorMode);
|
||||||
foreach ($this->modelHydrators as $modelHydrator) {
|
|
||||||
$model->addModelHydrator($modelHydrator);
|
|
||||||
}
|
|
||||||
foreach ($this->itemHydrators as $itemHydrator) {
|
|
||||||
$model->addItemHydrator($itemHydrator);
|
|
||||||
}
|
|
||||||
|
|
||||||
$model->setQuery($query);
|
$model->setQuery($query);
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@
|
|||||||
|
|
||||||
namespace App\Invoice;
|
namespace App\Invoice;
|
||||||
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
|
||||||
|
|
||||||
#[AutoconfigureTag]
|
|
||||||
interface InvoiceModelHydrator
|
interface InvoiceModelHydrator
|
||||||
{
|
{
|
||||||
public function hydrate(InvoiceModel $model): array;
|
public function hydrate(InvoiceModel $model): array;
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
|
|||||||
public const CUSTOMER_ORDER_ALLOWED = [
|
public const CUSTOMER_ORDER_ALLOWED = [
|
||||||
'name',
|
'name',
|
||||||
'description' => 'comment',
|
'description' => 'comment',
|
||||||
'country',
|
'country', 'number',
|
||||||
'number',
|
|
||||||
'homepage',
|
'homepage',
|
||||||
'email',
|
'email',
|
||||||
'mobile',
|
'mobile',
|
||||||
'fax',
|
'fax',
|
||||||
'phone',
|
'phone',
|
||||||
'currency',
|
'currency',
|
||||||
|
'address',
|
||||||
'contact',
|
'contact',
|
||||||
'company',
|
'company',
|
||||||
'vat_id',
|
'vat_id',
|
||||||
|
|||||||
64
src/Serializer/Serializer.php
Normal file
64
src/Serializer/Serializer.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\Serializer;
|
||||||
|
|
||||||
|
use JMS\Serializer\Context;
|
||||||
|
use JMS\Serializer\DeserializationContext;
|
||||||
|
use JMS\Serializer\SerializationContext;
|
||||||
|
use JMS\Serializer\Serializer as CoreSerializer;
|
||||||
|
use JMS\Serializer\SerializerInterface as CoreSerializerInterface;
|
||||||
|
|
||||||
|
class Serializer implements SerializerInterface
|
||||||
|
{
|
||||||
|
public function __construct(private readonly CoreSerializerInterface $serializer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepareContext(Context $jmsContext, array $context = []): void
|
||||||
|
{
|
||||||
|
$groups = ['Default'];
|
||||||
|
if (\array_key_exists('groups', $context)) {
|
||||||
|
$groups = $context['groups'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$jmsContext->setGroups($groups);
|
||||||
|
$jmsContext->enableMaxDepthChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(mixed $data, array $context = []): array
|
||||||
|
{
|
||||||
|
$jmsContext = SerializationContext::create();
|
||||||
|
$this->prepareContext($jmsContext, $context);
|
||||||
|
|
||||||
|
if ($this->serializer instanceof CoreSerializer) {
|
||||||
|
return $this->serializer->toArray($data, $jmsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = $this->serializer->serialize($data, 'json', $jmsContext);
|
||||||
|
|
||||||
|
return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serialize(mixed $data, string $format, array $context = []): string
|
||||||
|
{
|
||||||
|
$jmsContext = SerializationContext::create();
|
||||||
|
$this->prepareContext($jmsContext, $context);
|
||||||
|
|
||||||
|
return $this->serializer->serialize($data, $format, $jmsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed
|
||||||
|
{
|
||||||
|
$jmsContext = DeserializationContext::create();
|
||||||
|
$this->prepareContext($jmsContext, $context);
|
||||||
|
|
||||||
|
return $this->serializer->deserialize($data, $type, $format, $jmsContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Serializer/SerializerInterface.php
Normal file
17
src/Serializer/SerializerInterface.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\Serializer;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface as BaseSerializerInterface;
|
||||||
|
|
||||||
|
interface SerializerInterface extends BaseSerializerInterface
|
||||||
|
{
|
||||||
|
public function toArray(mixed $data, array $context = []): array;
|
||||||
|
}
|
||||||
109
src/Webhook/WebhookListener.php
Normal file
109
src/Webhook/WebhookListener.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?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\Webhook;
|
||||||
|
|
||||||
|
use App\Event\ActivityCreatePostEvent;
|
||||||
|
use App\Event\ActivityDeleteEvent;
|
||||||
|
use App\Event\ActivityUpdatePostEvent;
|
||||||
|
use App\Event\CustomerCreatePostEvent;
|
||||||
|
use App\Event\CustomerDeleteEvent;
|
||||||
|
use App\Event\CustomerUpdatePostEvent;
|
||||||
|
use App\Event\InvoiceCreatedEvent;
|
||||||
|
use App\Event\InvoiceDeleteEvent;
|
||||||
|
use App\Event\ProjectCreatePostEvent;
|
||||||
|
use App\Event\ProjectDeleteEvent;
|
||||||
|
use App\Event\ProjectUpdatePostEvent;
|
||||||
|
use App\Event\TeamCreatePostEvent;
|
||||||
|
use App\Event\TeamDeleteEvent;
|
||||||
|
use App\Event\TeamUpdatePostEvent;
|
||||||
|
use App\Event\TimesheetCreatePostEvent;
|
||||||
|
use App\Event\TimesheetStopPostEvent;
|
||||||
|
use App\Event\TimesheetUpdatePostEvent;
|
||||||
|
use App\Event\UserCreatePostEvent;
|
||||||
|
use App\Event\UserDeletePostEvent;
|
||||||
|
use App\Event\UserUpdatePostEvent;
|
||||||
|
use App\Webhook\Attribute\AsWebhook;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||||
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
|
final class WebhookListener implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public function __construct(private readonly WebhookService $webhookService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ActivityCreatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
ActivityDeleteEvent::class => ['triggerWebhook', 1000],
|
||||||
|
ActivityUpdatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
|
||||||
|
CustomerCreatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
CustomerDeleteEvent::class => ['triggerWebhook', 1000],
|
||||||
|
CustomerUpdatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
|
||||||
|
InvoiceCreatedEvent::class => ['triggerWebhook', 1000],
|
||||||
|
InvoiceDeleteEvent::class => ['triggerWebhook', 1000],
|
||||||
|
|
||||||
|
ProjectCreatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
ProjectDeleteEvent::class => ['triggerWebhook', 1000],
|
||||||
|
ProjectUpdatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
|
||||||
|
TimesheetCreatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
TimesheetStopPostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
TimesheetUpdatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
|
||||||
|
UserCreatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
UserDeletePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
UserUpdatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
|
||||||
|
TeamCreatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
TeamDeleteEvent::class => ['triggerWebhook', 1000],
|
||||||
|
TeamUpdatePostEvent::class => ['triggerWebhook', 1000],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function triggerWebhook(Event $event): void
|
||||||
|
{
|
||||||
|
$attribute = $this->findAttribute($event);
|
||||||
|
if ($attribute === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->webhookService->hasWebhook($attribute->name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expressionLanguage = new ExpressionLanguage();
|
||||||
|
$parsed = $expressionLanguage->parse($attribute->payload, ['object']);
|
||||||
|
$payload = $parsed->getNodes()->evaluate([], ['object' => $event]);
|
||||||
|
|
||||||
|
$this->webhookService->trigger($attribute->name, $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findAttribute(Event $event): ?AsWebhook
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$reflectionClass = new \ReflectionClass($event);
|
||||||
|
$attributes = $reflectionClass->getAttributes(AsWebhook::class);
|
||||||
|
if (\count($attributes) === 1) {
|
||||||
|
$attribute = $attributes[0];
|
||||||
|
$args = $attribute->getArguments();
|
||||||
|
|
||||||
|
return new AsWebhook($args['name'], $args['description'], $args['payload']);
|
||||||
|
}
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/Webhook/WebhookService.php
Normal file
58
src/Webhook/WebhookService.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?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\Webhook;
|
||||||
|
|
||||||
|
use App\Entity\WebhookConfiguration;
|
||||||
|
use App\Entity\WebhookEvent;
|
||||||
|
use App\Serializer\SerializerInterface;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\RemoteEvent\RemoteEvent;
|
||||||
|
use Symfony\Component\Webhook\Messenger\SendWebhookMessage;
|
||||||
|
use Symfony\Component\Webhook\Subscriber;
|
||||||
|
|
||||||
|
final class WebhookService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly SerializerInterface $serializer,
|
||||||
|
private readonly MessageBusInterface $bus
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasWebhook(string $name): bool
|
||||||
|
{
|
||||||
|
return $name === 'user.created';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function trigger(string $name, mixed $payload): void
|
||||||
|
{
|
||||||
|
$content = $this->serializer->toArray($payload, ['groups' => ['Default', 'Entity', 'Expanded']]);
|
||||||
|
|
||||||
|
foreach ($this->findEventsByName($name) as $event) {
|
||||||
|
$configuration = $event->getConfiguration();
|
||||||
|
$subscriber = new Subscriber($configuration->getUrl(), $configuration->getSecret());
|
||||||
|
$remoteEvent = new RemoteEvent($name, $name . '_' . uniqid(microtime()), $content);
|
||||||
|
$message = new SendWebhookMessage($subscriber, $remoteEvent);
|
||||||
|
$this->bus->dispatch($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findEventsByName(string $name): array
|
||||||
|
{
|
||||||
|
$url = 'TODO';
|
||||||
|
$secret = 'TODO';
|
||||||
|
|
||||||
|
$configuration = new WebhookConfiguration('n8n', $url, 'json', $secret, 'bearer');
|
||||||
|
|
||||||
|
return [
|
||||||
|
new WebhookEvent($name, $configuration),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
symfony.lock
24
symfony.lock
@@ -467,6 +467,18 @@
|
|||||||
"config/packages/mailer.yaml"
|
"config/packages/mailer.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/messenger": {
|
||||||
|
"version": "6.4",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.0",
|
||||||
|
"ref": "d8936e2e2230637ef97e5eecc0eea074eecae58b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/messenger.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/mime": {
|
"symfony/mime": {
|
||||||
"version": "v4.3.3"
|
"version": "v4.3.3"
|
||||||
},
|
},
|
||||||
@@ -598,6 +610,18 @@
|
|||||||
"ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6"
|
"ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"symfony/webhook": {
|
||||||
|
"version": "6.4",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.3",
|
||||||
|
"ref": "0bc853a74c98e448116ba53e118a7b6b3cdf9a03"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/routes/webhook.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/webpack-encore-bundle": {
|
"symfony/webpack-encore-bundle": {
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
'back': path('admin_activity')
|
'back': path('admin_activity')
|
||||||
} %}
|
} %}
|
||||||
{% embed formEditTemplate with formOptions %}
|
{% embed formEditTemplate with formOptions %}
|
||||||
{% import "macros/form_helper.html.twig" as form_helper %}
|
|
||||||
{% block form_body %}
|
{% block form_body %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
@@ -50,18 +49,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ form_helper.metafields(form, null) }}
|
|
||||||
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'activity_invoice_settings'} %}
|
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
|
||||||
{% import "macros/form_helper.html.twig" as form_helper %}
|
|
||||||
{% block title %}{{ 'invoices'|trans }}{% endblock %}
|
|
||||||
{% block body %}
|
|
||||||
{% if form.invoiceText is defined %}
|
|
||||||
{{ form_row(form.invoiceText) }}
|
|
||||||
{% endif %}
|
|
||||||
{{ form_helper.metafields(form, 'invoice') }}
|
|
||||||
{% endblock %}
|
|
||||||
{% endembed %}
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{{ form_row(form.visible) }}
|
{{ form_row(form.visible) }}
|
||||||
@@ -70,6 +57,18 @@
|
|||||||
{{ form_row(form.billable) }}
|
{{ form_row(form.billable) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if form.metaFields is defined and form.metaFields is not empty %}
|
||||||
|
{{ form_row(form.metaFields) }}
|
||||||
|
{% endif %}
|
||||||
|
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'activity_invoice_settings'} %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% block title %}{{ 'invoices'|trans }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{% if form.invoiceText is defined %}
|
||||||
|
{{ form_row(form.invoiceText) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endembed %}
|
||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
'back': path('admin_customer')
|
'back': path('admin_customer')
|
||||||
} %}
|
} %}
|
||||||
{% embed formEditTemplate with formOptions %}
|
{% embed formEditTemplate with formOptions %}
|
||||||
{% import "macros/form_helper.html.twig" as form_helper %}
|
|
||||||
{% block form_body %}
|
{% block form_body %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
@@ -76,10 +75,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ form_helper.metafields(form, null) }}
|
{% if form.metaFields is defined and form.metaFields is not empty %}
|
||||||
|
{{ form_row(form.metaFields) }}
|
||||||
|
{% endif %}
|
||||||
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'customer_address_settings'} %}
|
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'customer_address_settings'} %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
{% import "macros/form_helper.html.twig" as form_helper %}
|
|
||||||
{% block title %}{{ 'address'|trans }}{% endblock %}
|
{% block title %}{{ 'address'|trans }}{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -125,12 +125,11 @@
|
|||||||
{{ form_row(form.timezone) }}
|
{{ form_row(form.timezone) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ form_helper.metafields(form, 'address') }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'customer_invoice_settings'} %}
|
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'customer_invoice_settings'} %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
{% import "macros/form_helper.html.twig" as form_helper %}
|
|
||||||
{% block title %}{{ 'invoices'|trans }}{% endblock %}
|
{% block title %}{{ 'invoices'|trans }}{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if form.buyerReference is defined %}
|
{% if form.buyerReference is defined %}
|
||||||
@@ -142,7 +141,6 @@
|
|||||||
{% if form.invoiceText is defined %}
|
{% if form.invoiceText is defined %}
|
||||||
{{ form_row(form.invoiceText) }}
|
{{ form_row(form.invoiceText) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ form_helper.metafields(form, 'invoice') }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
{{ entry.vatId }}
|
{{ entry.vatId }}
|
||||||
{% elseif column == 'contact' %}
|
{% elseif column == 'contact' %}
|
||||||
{{ entry.contact }}
|
{{ entry.contact }}
|
||||||
{% elseif column == 'city' %}
|
{% elseif column == 'address' %}
|
||||||
{{ entry.postcode }} {{ entry.city }}
|
{{ entry.address }}
|
||||||
{% elseif column == 'country' %}
|
{% elseif column == 'country' %}
|
||||||
{{ entry.country|country_name }}
|
{{ entry.country|country_name }}
|
||||||
{% elseif column == 'currency' %}
|
{% elseif column == 'currency' %}
|
||||||
|
|||||||
@@ -474,9 +474,7 @@
|
|||||||
|
|
||||||
{%- block meta_fields_collection_widget -%}
|
{%- block meta_fields_collection_widget -%}
|
||||||
{% for meta in form|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
|
{% for meta in form|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
|
||||||
{% if not meta.rendered %}
|
{{ form_row(meta.value, {'row_attr': {'class': 'mb-3 ' ~ form.parent.vars.name ~ '_row_' ~ form.vars.name ~ '_' ~ meta.vars.name}}) }}
|
||||||
{{ form_row(meta.value, {'row_attr': {'class': 'mb-3 ' ~ form.parent.vars.name ~ '_row_' ~ form.vars.name ~ '_' ~ meta.vars.name}}) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- endblock meta_fields_collection_widget %}
|
{%- endblock meta_fields_collection_widget %}
|
||||||
|
|
||||||
|
|||||||
@@ -135,25 +135,20 @@ mpdf-->
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
|
{% if not invoice['invoice.tax_hide'] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-right">
|
<td colspan="4" class="text-right">
|
||||||
{{ 'invoice.subtotal'|trans }}
|
{{ 'invoice.subtotal'|trans }}
|
||||||
</td>
|
</td>
|
||||||
<td class="last text-right">{{ invoice['invoice.subtotal'] }}</td>
|
<td class="last text-right">{{ invoice['invoice.subtotal'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for taxRow in invoice['invoice.tax_rows'] %}
|
<tr>
|
||||||
{% if taxRow['show'] %}
|
<td colspan="4" class="text-right">
|
||||||
<tr>
|
{{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%)
|
||||||
<td colspan="4" class="text-right">
|
</td>
|
||||||
{{ taxRow['name']|trans }} ({{ taxRow['rate'] }}%)
|
<td class="last text-right">{{ invoice['invoice.tax'] }}</td>
|
||||||
{% if taxRow['note'] is not null %}
|
</tr>
|
||||||
<sup>[{{ taxRow['counter'] }}]</sup>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="last text-right">{{ taxRow['amount']|money(taxRow['currency']) }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-right text-nowrap">
|
<td colspan="4" class="text-right text-nowrap">
|
||||||
<strong>{{ 'invoice.total'|trans }}</strong>
|
<strong>{{ 'invoice.total'|trans }}</strong>
|
||||||
@@ -165,17 +160,10 @@ mpdf-->
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% for taxRow in invoice['invoice.tax_rows'] %}
|
|
||||||
{% if taxRow['note'] is not null %}
|
|
||||||
<p>
|
|
||||||
<sup>[{{ taxRow['counter'] }}]</sup>
|
|
||||||
{{ taxRow['note']|trans }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if invoice['template.payment_terms'] is not empty %}
|
{% if invoice['template.payment_terms'] is not empty %}
|
||||||
{{ invoice['template.payment_terms']|md2html }}
|
<div class="paymentTerms">
|
||||||
|
{{ invoice['template.payment_terms']|md2html }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<h2 class="page-header">
|
<h2 class="page-header">
|
||||||
<span>{{ invoice['template.title'] }}</span>
|
<span contenteditable="true">{{ invoice['template.title'] }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-5">
|
<div class="col-sm-5">
|
||||||
{{ 'invoice.to'|trans }}
|
{{ 'invoice.to'|trans }}
|
||||||
<address>
|
<address contenteditable="true">
|
||||||
<strong>{{ invoice['customer.company']|default(invoice['customer.name']) }}</strong><br>
|
<strong>{{ invoice['customer.company']|default(invoice['customer.name']) }}</strong><br>
|
||||||
{{ invoice['customer.address']|nl2br }}
|
{{ invoice['customer.address']|nl2br }}
|
||||||
{% set country = invoice['customer.country']|country_name(invoice['invoice.language']) %}
|
{% set country = invoice['customer.country']|country_name(invoice['invoice.language']) %}
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<div class="col-sm-2"></div>
|
<div class="col-sm-2"></div>
|
||||||
<div class="col-sm-5">
|
<div class="col-sm-5">
|
||||||
{{ 'invoice.from'|trans }}
|
{{ 'invoice.from'|trans }}
|
||||||
<address>
|
<address contenteditable="true">
|
||||||
<strong>{{ invoice['template.company'] }}</strong><br>
|
<strong>{{ invoice['template.company'] }}</strong><br>
|
||||||
{{ invoice['template.address']|trim|nl2br }}
|
{{ invoice['template.address']|trim|nl2br }}
|
||||||
{% if invoice['template.country'] is not null %}
|
{% if invoice['template.country'] is not null %}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-5">
|
<div class="col-sm-5">
|
||||||
<p>
|
<p contenteditable="true">
|
||||||
<strong>{{ 'date'|trans }}:</strong>
|
<strong>{{ 'date'|trans }}:</strong>
|
||||||
{{ invoice['invoice.date'] }}
|
{{ invoice['invoice.date'] }}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
{% for invoiceLineItem in entries %}
|
{% for invoiceLineItem in entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td nowrap class="text-nowrap">{{ invoiceLineItem['entry.begin'] }}</td>
|
<td nowrap class="text-nowrap">{{ invoiceLineItem['entry.begin'] }}</td>
|
||||||
<td>
|
<td contenteditable="true">
|
||||||
{% if invoiceLineItem['entry.description'] is not empty %}
|
{% if invoiceLineItem['entry.description'] is not empty %}
|
||||||
{{ invoiceLineItem['entry.description']|nl2br }}
|
{{ invoiceLineItem['entry.description']|nl2br }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -115,25 +115,20 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
|
{% if not invoice['invoice.tax_hide'] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-end">
|
<td colspan="4" class="text-end">
|
||||||
{{ 'invoice.subtotal'|trans }}
|
{{ 'invoice.subtotal'|trans }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">{{ invoice['invoice.subtotal'] }}</td>
|
<td class="text-end">{{ invoice['invoice.subtotal'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for taxRow in invoice['invoice.tax_rows'] %}
|
<tr>
|
||||||
{% if taxRow['show'] %}
|
<td colspan="4" class="text-end">
|
||||||
<tr>
|
{{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%)
|
||||||
<td colspan="4" class="text-end">
|
</td>
|
||||||
{{ taxRow['name']|trans }} ({{ taxRow['rate'] }}%)
|
<td class="text-end">{{ invoice['invoice.tax'] }}</td>
|
||||||
{% if taxRow['note'] is not null %}
|
</tr>
|
||||||
<sup>[{{ taxRow['counter'] }}]</sup>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="last text-right">{{ taxRow['amount']|money(taxRow['currency']) }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-end text-nowrap">
|
<td colspan="4" class="text-end text-nowrap">
|
||||||
<strong>{{ 'invoice.total'|trans }}</strong>
|
<strong>{{ 'invoice.total'|trans }}</strong>
|
||||||
@@ -149,16 +144,10 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
{% for taxRow in invoice['invoice.tax_rows'] %}
|
|
||||||
{% if taxRow['note'] is not null %}
|
|
||||||
<p>
|
|
||||||
<sup>[{{ taxRow['counter'] }}]</sup>
|
|
||||||
{{ taxRow['note']|trans }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if invoice['template.payment_terms'] is not empty %}
|
{% if invoice['template.payment_terms'] is not empty %}
|
||||||
{{ invoice['template.payment_terms']|md2html }}
|
<div contenteditable="true" class="paymentTerms">
|
||||||
|
{{ invoice['template.payment_terms']|md2html }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -150,25 +150,20 @@ mpdf-->
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
|
{% if not invoice['invoice.tax_hide'] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="text-right">
|
<td colspan="3" class="text-right">
|
||||||
{{ 'invoice.subtotal'|trans }}
|
{{ 'invoice.subtotal'|trans }}
|
||||||
</td>
|
</td>
|
||||||
<td class="last text-right">{{ invoice['invoice.subtotal'] }}</td>
|
<td class="last text-right">{{ invoice['invoice.subtotal'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for taxRow in invoice['invoice.tax_rows'] %}
|
<tr>
|
||||||
{% if taxRow['show'] %}
|
<td colspan="3" class="text-right">
|
||||||
<tr>
|
{{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%)
|
||||||
<td colspan="3" class="text-right">
|
</td>
|
||||||
{{ taxRow['name']|trans }} ({{ taxRow['rate'] }}%)
|
<td class="last text-right">{{ invoice['invoice.tax'] }}</td>
|
||||||
{% if taxRow['note'] is not null %}
|
</tr>
|
||||||
<sup>[{{ taxRow['counter'] }}]</sup>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="last text-right">{{ taxRow['amount']|money(taxRow['currency']) }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="text-right text-nowrap">
|
<td colspan="3" class="text-right text-nowrap">
|
||||||
<strong>{{ 'invoice.total'|trans }}</strong>
|
<strong>{{ 'invoice.total'|trans }}</strong>
|
||||||
@@ -180,17 +175,10 @@ mpdf-->
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% for taxRow in invoice['invoice.tax_rows'] %}
|
|
||||||
{% if taxRow['note'] is not null %}
|
|
||||||
<p>
|
|
||||||
<sup>[{{ taxRow['counter'] }}]</sup>
|
|
||||||
{{ taxRow['note']|trans }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if invoice['template.payment_terms'] is not empty %}
|
{% if invoice['template.payment_terms'] is not empty %}
|
||||||
{{ invoice['template.payment_terms']|md2html }}
|
<div class="paymentTerms">
|
||||||
|
{{ invoice['template.payment_terms']|md2html }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{% macro metafields(form, section) %}
|
|
||||||
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
|
|
||||||
{% if meta.vars.data.section is same as (section) %}
|
|
||||||
{{ form_row(meta.value, {'row_attr': {'class': 'mb-3 ' ~ form.vars.name ~ '_row_' ~ form.metaFields.vars.name ~ '_' ~ meta.vars.name}}) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endmacro %}
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
'back': path('admin_project')
|
'back': path('admin_project')
|
||||||
} %}
|
} %}
|
||||||
{% embed formEditTemplate with formOptions %}
|
{% embed formEditTemplate with formOptions %}
|
||||||
{% import "macros/form_helper.html.twig" as form_helper %}
|
|
||||||
{% block form_body %}
|
{% block form_body %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
@@ -59,10 +58,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ form_helper.metafields(form, null) }}
|
{% if form.metaFields is defined and form.metaFields is not empty %}
|
||||||
|
{{ form_row(form.metaFields) }}
|
||||||
|
{% endif %}
|
||||||
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'project_invoice_settings'} %}
|
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'project_invoice_settings'} %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
{% import "macros/form_helper.html.twig" as form_helper %}
|
|
||||||
{% block title %}{{ 'invoices'|trans }}{% endblock %}
|
{% block title %}{{ 'invoices'|trans }}{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -76,7 +76,6 @@
|
|||||||
{% if form.invoiceText is defined %}
|
{% if form.invoiceText is defined %}
|
||||||
{{ form_row(form.invoiceText) }}
|
{{ form_row(form.invoiceText) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ form_helper.metafields(form, 'invoice') }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ abstract class AbstractMetaEntityTestCase extends TestCase
|
|||||||
self::assertFalse($sut->isVisible());
|
self::assertFalse($sut->isVisible());
|
||||||
self::assertFalse($sut->isRequired());
|
self::assertFalse($sut->isRequired());
|
||||||
self::assertEquals(0, $sut->getOrder());
|
self::assertEquals(0, $sut->getOrder());
|
||||||
self::assertNull($sut->getSection());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetterAndGetter(): void
|
public function testSetterAndGetter(): void
|
||||||
@@ -84,11 +83,6 @@ abstract class AbstractMetaEntityTestCase extends TestCase
|
|||||||
$entity = $this->getEntity();
|
$entity = $this->getEntity();
|
||||||
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setEntity($entity));
|
self::assertInstanceOf(MetaTableTypeInterface::class, $sut->setEntity($entity));
|
||||||
self::assertSame($entity, $sut->getEntity());
|
self::assertSame($entity, $sut->getEntity());
|
||||||
|
|
||||||
$sut->setSection('foo');
|
|
||||||
self::assertEquals('foo', $sut->getSection());
|
|
||||||
$sut->setSection(null);
|
|
||||||
self::assertNull($sut->getSection());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMerge(): void
|
public function testMerge(): void
|
||||||
@@ -96,19 +90,18 @@ abstract class AbstractMetaEntityTestCase extends TestCase
|
|||||||
$entity1 = $this->getEntity();
|
$entity1 = $this->getEntity();
|
||||||
$entity2 = $this->getEntity();
|
$entity2 = $this->getEntity();
|
||||||
$meta1 = $this->getMetaEntity();
|
$meta1 = $this->getMetaEntity();
|
||||||
$meta1->setName('foo');
|
$meta1
|
||||||
$meta1->setValue('bar');
|
->setName('foo')
|
||||||
$meta1->setType('blub');
|
->setValue('bar')
|
||||||
$meta1->setEntity($entity1);
|
->setType('blub')
|
||||||
$meta1->setConstraints([new NotNull()]);
|
->setEntity($entity1)
|
||||||
$meta1->setOptions(['foo' => 'bar', 'bar' => 'foo']);
|
->setConstraints([new NotNull()])
|
||||||
$meta1->setSection('hello');
|
->setOptions(['foo' => 'bar', 'bar' => 'foo'])
|
||||||
|
;
|
||||||
self::assertEquals('foo', $meta1->getName());
|
self::assertEquals('foo', $meta1->getName());
|
||||||
self::assertEquals('bar', $meta1->getValue());
|
self::assertEquals('bar', $meta1->getValue());
|
||||||
self::assertEquals('blub', $meta1->getType());
|
self::assertEquals('blub', $meta1->getType());
|
||||||
self::assertEquals('foo', $meta1->getLabel());
|
self::assertEquals('foo', $meta1->getLabel());
|
||||||
self::assertEquals('hello', $meta1->getSection());
|
|
||||||
self::assertFalse($meta1->isRequired());
|
self::assertFalse($meta1->isRequired());
|
||||||
self::assertFalse($meta1->isVisible());
|
self::assertFalse($meta1->isVisible());
|
||||||
self::assertSame($entity1, $meta1->getEntity());
|
self::assertSame($entity1, $meta1->getEntity());
|
||||||
@@ -116,17 +109,18 @@ abstract class AbstractMetaEntityTestCase extends TestCase
|
|||||||
self::assertCount(2, $meta1->getOptions());
|
self::assertCount(2, $meta1->getOptions());
|
||||||
|
|
||||||
$meta2 = $this->getMetaEntity();
|
$meta2 = $this->getMetaEntity();
|
||||||
$meta2->setName('foo2');
|
$meta2
|
||||||
$meta2->setLabel('TRALALA');
|
->setName('foo2')
|
||||||
$meta2->setValue('bar2');
|
->setLabel('TRALALA')
|
||||||
$meta2->setType('blub2');
|
->setValue('bar2')
|
||||||
$meta2->setSection('world');
|
->setType('blub2')
|
||||||
$meta2->setEntity($entity2);
|
->setEntity($entity2)
|
||||||
$meta2->setIsRequired(true);
|
->setIsRequired(true)
|
||||||
$meta2->setIsVisible(true);
|
->setIsVisible(true)
|
||||||
$meta2->setOrder(93);
|
->setOrder(93)
|
||||||
$meta2->setConstraints([new NotBlank(), new Length(['min' => 1])]);
|
->setConstraints([new NotBlank(), new Length(['min' => 1])])
|
||||||
$meta2->setOptions(['foo1' => 'bar1']);
|
->setOptions(['foo1' => 'bar1'])
|
||||||
|
;
|
||||||
|
|
||||||
self::assertInstanceOf(MetaTableTypeInterface::class, $meta1->merge($meta2));
|
self::assertInstanceOf(MetaTableTypeInterface::class, $meta1->merge($meta2));
|
||||||
|
|
||||||
@@ -134,7 +128,6 @@ abstract class AbstractMetaEntityTestCase extends TestCase
|
|||||||
self::assertEquals('bar', $meta1->getValue());
|
self::assertEquals('bar', $meta1->getValue());
|
||||||
self::assertEquals('blub2', $meta1->getType());
|
self::assertEquals('blub2', $meta1->getType());
|
||||||
self::assertEquals('TRALALA', $meta1->getLabel());
|
self::assertEquals('TRALALA', $meta1->getLabel());
|
||||||
self::assertEquals('world', $meta1->getSection());
|
|
||||||
self::assertEquals(93, $meta1->getOrder());
|
self::assertEquals(93, $meta1->getOrder());
|
||||||
self::assertTrue($meta1->isRequired());
|
self::assertTrue($meta1->isRequired());
|
||||||
self::assertTrue($meta1->isVisible());
|
self::assertTrue($meta1->isVisible());
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ zip 12345 looney toon', $sut->getFormattedAddress());
|
|||||||
['visible', 'boolean'],
|
['visible', 'boolean'],
|
||||||
['comment', 'string'],
|
['comment', 'string'],
|
||||||
['billable', 'boolean'],
|
['billable', 'boolean'],
|
||||||
['buyerReference', 'string'],
|
['buyer_reference', 'string'],
|
||||||
];
|
];
|
||||||
|
|
||||||
self::assertCount(\count($expected), $columns);
|
self::assertCount(\count($expected), $columns);
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ use App\Entity\Customer;
|
|||||||
use App\Entity\CustomerMeta;
|
use App\Entity\CustomerMeta;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
use App\Entity\InvoiceTemplateMeta;
|
use App\Entity\InvoiceTemplateMeta;
|
||||||
use App\Entity\Tax;
|
|
||||||
use App\Entity\TaxType;
|
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -46,48 +44,6 @@ class InvoiceTemplateTest extends TestCase
|
|||||||
self::assertNull($sut->getMetaField('foo'));
|
self::assertNull($sut->getMetaField('foo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTaxRates(): void
|
|
||||||
{
|
|
||||||
$sut = new InvoiceTemplate();
|
|
||||||
|
|
||||||
$rates = $sut->getTaxRates();
|
|
||||||
self::assertCount(1, $rates);
|
|
||||||
self::assertEquals(TaxType::STANDARD, $rates[0]->getType());
|
|
||||||
self::assertEquals(0.00, $rates[0]->getRate());
|
|
||||||
self::assertEquals('vat', $rates[0]->getName());
|
|
||||||
self::assertNull($rates[0]->getNote());
|
|
||||||
|
|
||||||
$tax1 = new Tax(
|
|
||||||
TaxType::REVERSE,
|
|
||||||
$this->vat ?? 0.00,
|
|
||||||
'tax.name.reverse_charge',
|
|
||||||
true,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
$tax2 = new Tax(
|
|
||||||
TaxType::EXEMPT,
|
|
||||||
$this->vat ?? 0.00,
|
|
||||||
'tax.name.exempt',
|
|
||||||
true,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
$sut->setTaxRates([$tax1, $tax2]);
|
|
||||||
$rates = $sut->getTaxRates();
|
|
||||||
|
|
||||||
self::assertCount(2, $rates);
|
|
||||||
|
|
||||||
self::assertEquals(TaxType::REVERSE, $rates[0]->getType());
|
|
||||||
self::assertEquals(0.00, $rates[0]->getRate());
|
|
||||||
self::assertEquals('tax.name.reverse_charge', $rates[0]->getName());
|
|
||||||
self::assertNull($rates[0]->getNote());
|
|
||||||
|
|
||||||
self::assertEquals(TaxType::EXEMPT, $rates[1]->getType());
|
|
||||||
self::assertEquals(0.00, $rates[1]->getRate());
|
|
||||||
self::assertEquals('tax.name.exempt', $rates[1]->getName());
|
|
||||||
self::assertNull($rates[1]->getNote());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSetterAndGetter(): void
|
public function testSetterAndGetter(): void
|
||||||
{
|
{
|
||||||
$sut = new InvoiceTemplate();
|
$sut = new InvoiceTemplate();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
{{ encore_entry_css_source('invoice-pdf')|raw }}
|
{{ encore_entry_css_source('invoice')|raw }}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="invoice_print">
|
<body class="invoice_print">
|
||||||
|
|||||||
@@ -11,13 +11,6 @@ namespace App\Tests\Mocks;
|
|||||||
|
|
||||||
use App\Activity\ActivityStatisticService;
|
use App\Activity\ActivityStatisticService;
|
||||||
use App\Customer\CustomerStatisticService;
|
use App\Customer\CustomerStatisticService;
|
||||||
use App\Invoice\Hydrator\InvoiceItemDefaultHydrator;
|
|
||||||
use App\Invoice\Hydrator\InvoiceModelActivityHydrator;
|
|
||||||
use App\Invoice\Hydrator\InvoiceModelCustomerHydrator;
|
|
||||||
use App\Invoice\Hydrator\InvoiceModelDefaultHydrator;
|
|
||||||
use App\Invoice\Hydrator\InvoiceModelIssuerHydrator;
|
|
||||||
use App\Invoice\Hydrator\InvoiceModelProjectHydrator;
|
|
||||||
use App\Invoice\Hydrator\InvoiceModelUserHydrator;
|
|
||||||
use App\Invoice\InvoiceModelFactory;
|
use App\Invoice\InvoiceModelFactory;
|
||||||
use App\Project\ProjectStatisticService;
|
use App\Project\ProjectStatisticService;
|
||||||
use App\Timesheet\RateCalculator\DecimalRateCalculator;
|
use App\Timesheet\RateCalculator\DecimalRateCalculator;
|
||||||
@@ -32,19 +25,8 @@ class InvoiceModelFactoryFactory extends AbstractMockFactory
|
|||||||
$projectStatistic = $this->getMockBuilder(ProjectStatisticService::class)->disableOriginalConstructor()->getMock();
|
$projectStatistic = $this->getMockBuilder(ProjectStatisticService::class)->disableOriginalConstructor()->getMock();
|
||||||
/** @var ActivityStatisticService $activityStatistic */
|
/** @var ActivityStatisticService $activityStatistic */
|
||||||
$activityStatistic = $this->getMockBuilder(ActivityStatisticService::class)->disableOriginalConstructor()->getMock();
|
$activityStatistic = $this->getMockBuilder(ActivityStatisticService::class)->disableOriginalConstructor()->getMock();
|
||||||
|
$rateMode = new DecimalRateCalculator();
|
||||||
|
|
||||||
$modelHydrators = [
|
return new InvoiceModelFactory($customerStatistic, $projectStatistic, $activityStatistic, $rateMode);
|
||||||
new InvoiceModelDefaultHydrator(),
|
|
||||||
new InvoiceModelCustomerHydrator($customerStatistic),
|
|
||||||
new InvoiceModelIssuerHydrator(),
|
|
||||||
new InvoiceModelProjectHydrator($projectStatistic),
|
|
||||||
new InvoiceModelActivityHydrator($activityStatistic),
|
|
||||||
new InvoiceModelUserHydrator(),
|
|
||||||
];
|
|
||||||
$itemHydrators = [
|
|
||||||
new InvoiceItemDefaultHydrator(),
|
|
||||||
];
|
|
||||||
|
|
||||||
return new InvoiceModelFactory(new DecimalRateCalculator(), $modelHydrators, $itemHydrators);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2046,26 +2046,6 @@
|
|||||||
<source>payment_account_name</source>
|
<source>payment_account_name</source>
|
||||||
<target state="translated">Titular del compte o targeta</target>
|
<target state="translated">Titular del compte o targeta</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="QnN_ONc" resname="invoice_tax_type" xml:space="preserve">
|
|
||||||
<source>invoice_tax_type</source>
|
|
||||||
<target state="translated">Estat fiscal</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Mi7y8Q3" resname="tax.type.exempt" xml:space="preserve">
|
|
||||||
<source>tax.type.exempt</source>
|
|
||||||
<target state="translated">Exempt</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="lU1Njcy" resname="tax.type.standard" xml:space="preserve">
|
|
||||||
<source>tax.type.standard</source>
|
|
||||||
<target state="translated">Sotmès a impostos</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="E470U8W" resname="tax.type.reverse" xml:space="preserve">
|
|
||||||
<source>tax.type.reverse</source>
|
|
||||||
<target state="translated">Inversió del subjecte passiu</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="hi31a3v" resname="tax.note.reverse_charge" xml:space="preserve">
|
|
||||||
<source>tax.note.reverse_charge</source>
|
|
||||||
<target state="translated">Impost a pagar en règim d’inversió del subjecte passiu</target>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -2046,26 +2046,6 @@
|
|||||||
<source>electronic_invoice</source>
|
<source>electronic_invoice</source>
|
||||||
<target>E-Rechnung</target>
|
<target>E-Rechnung</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="QnN_ONc" resname="invoice_tax_type">
|
|
||||||
<source>invoice_tax_type</source>
|
|
||||||
<target>Steuerstatus</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Mi7y8Q3" resname="tax.type.exempt">
|
|
||||||
<source>tax.type.exempt</source>
|
|
||||||
<target>Ausgenommen</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="lU1Njcy" resname="tax.type.standard">
|
|
||||||
<source>tax.type.standard</source>
|
|
||||||
<target>Steuerpflichtig</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="E470U8W" resname="tax.type.reverse">
|
|
||||||
<source>tax.type.reverse</source>
|
|
||||||
<target>Umkehrung der Steuerschuld</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="hi31a3v" resname="tax.note.reverse_charge">
|
|
||||||
<source>tax.note.reverse_charge</source>
|
|
||||||
<target>Steuer zahlbar auf Reverse-Charge-Basis</target>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -2046,26 +2046,6 @@
|
|||||||
<source>electronic_invoice</source>
|
<source>electronic_invoice</source>
|
||||||
<target>E-Invoice</target>
|
<target>E-Invoice</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="QnN_ONc" resname="invoice_tax_type">
|
|
||||||
<source>invoice_tax_type</source>
|
|
||||||
<target>Tax status</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Mi7y8Q3" resname="tax.type.exempt">
|
|
||||||
<source>tax.type.exempt</source>
|
|
||||||
<target>Exempt</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="lU1Njcy" resname="tax.type.standard">
|
|
||||||
<source>tax.type.standard</source>
|
|
||||||
<target>Taxable</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="E470U8W" resname="tax.type.reverse">
|
|
||||||
<source>tax.type.reverse</source>
|
|
||||||
<target>Reverse charge</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="hi31a3v" resname="tax.note.reverse_charge">
|
|
||||||
<source>tax.note.reverse_charge</source>
|
|
||||||
<target>Tax to be paid on reverse charge basis</target>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -2046,26 +2046,6 @@
|
|||||||
<source>electronic_invoice</source>
|
<source>electronic_invoice</source>
|
||||||
<target state="final">Fattura elettronica</target>
|
<target state="final">Fattura elettronica</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="QnN_ONc" resname="invoice_tax_type" xml:space="preserve" approved="yes">
|
|
||||||
<source>invoice_tax_type</source>
|
|
||||||
<target state="final">Stato fiscale</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Mi7y8Q3" resname="tax.type.exempt" xml:space="preserve" approved="yes">
|
|
||||||
<source>tax.type.exempt</source>
|
|
||||||
<target state="final">Esonera</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="lU1Njcy" resname="tax.type.standard" xml:space="preserve" approved="yes">
|
|
||||||
<source>tax.type.standard</source>
|
|
||||||
<target state="final">Tassabile</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="E470U8W" resname="tax.type.reverse" xml:space="preserve" approved="yes">
|
|
||||||
<source>tax.type.reverse</source>
|
|
||||||
<target state="final">Addebito inverso</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="hi31a3v" resname="tax.note.reverse_charge" xml:space="preserve" approved="yes">
|
|
||||||
<source>tax.note.reverse_charge</source>
|
|
||||||
<target state="final">Tassa da pagare in base all'addebito inverso</target>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -310,10 +310,6 @@
|
|||||||
<source>copy_teams_on_create</source>
|
<source>copy_teams_on_create</source>
|
||||||
<target state="translated">Hereta els equips de l'usuari connectat en crear noves entrades</target>
|
<target state="translated">Hereta els equips de l'usuari connectat en crear noves entrades</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6AaVxB2" resname="invoice.rounding_mode" approved="no" xml:space="preserve">
|
|
||||||
<source>invoice.rounding_mode</source>
|
|
||||||
<target state="translated">Mode d’arrodoniment</target>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -310,10 +310,6 @@
|
|||||||
<source>activity.number_format</source>
|
<source>activity.number_format</source>
|
||||||
<target state="translated">Formato del número de actividad</target>
|
<target state="translated">Formato del número de actividad</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6AaVxB2" resname="invoice.rounding_mode" approved="no" xml:space="preserve">
|
|
||||||
<source>invoice.rounding_mode</source>
|
|
||||||
<target state="translated">Modo de redondeo</target>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user