invoices: choose language and duration format (#1438)
This commit is contained in:
@@ -22,10 +22,10 @@
|
||||
"friendsofsymfony/rest-bundle": "^2.5",
|
||||
"friendsofsymfony/user-bundle": "dev-master",
|
||||
"gedmo/doctrine-extensions": "^2.4",
|
||||
"hslavich/oneloginsaml-bundle": "^1.4",
|
||||
"jms/metadata": "^2.0",
|
||||
"jms/serializer-bundle": "^3.2",
|
||||
"kevinpapst/adminlte-bundle": "^3.0",
|
||||
"hslavich/oneloginsaml-bundle": "^1.4",
|
||||
"kimai/kimai2-composer": "^0.1",
|
||||
"laravolt/avatar": "^3.0",
|
||||
"league/csv": "^9.4",
|
||||
@@ -57,7 +57,9 @@
|
||||
"symfony/validator": "^4.0",
|
||||
"symfony/webpack-encore-bundle": "^1.5",
|
||||
"symfony/yaml": "^4.0",
|
||||
"twig/extensions": "^1.5",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
"twig/intl-extra": "^3.0",
|
||||
"twig/string-extra": "^3.0",
|
||||
"white-october/pagerfanta-bundle": "^1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -86,7 +88,7 @@
|
||||
],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.2"
|
||||
"php": "7.2.9"
|
||||
},
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
|
||||
349
composer.lock
generated
349
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "0367a6d800c3a64f39ccd00564953d04",
|
||||
"content-hash": "937e678d15c03f8508dd269116d57d82",
|
||||
"packages": [
|
||||
{
|
||||
"name": "beberlei/doctrineextensions",
|
||||
@@ -6942,6 +6942,66 @@
|
||||
],
|
||||
"time": "2019-11-27T13:56:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.13.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "45c566a1ca16273f7ea6b930e013462e00e14502"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/45c566a1ca16273f7ea6b930e013462e00e14502",
|
||||
"reference": "45c566a1ca16273f7ea6b930e013462e00e14502",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.13-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's grapheme_* functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"grapheme",
|
||||
"intl",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"time": "2019-11-27T13:56:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-icu",
|
||||
"version": "v1.13.1",
|
||||
@@ -7062,6 +7122,69 @@
|
||||
],
|
||||
"time": "2019-11-27T13:56:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.13.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "cfe6ad557c15f3797f667e9518ce759aa04ae4f3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/cfe6ad557c15f3797f667e9518ce759aa04ae4f3",
|
||||
"reference": "cfe6ad557c15f3797f667e9518ce759aa04ae4f3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.13-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's Normalizer class and related functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"intl",
|
||||
"normalizer",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"time": "2019-11-27T13:56:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.13.1",
|
||||
@@ -7977,6 +8100,70 @@
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2020-01-04T13:00:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v5.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "978261d3c4ffd64f99dea0e78b026690a67fe7fe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/978261d3c4ffd64f99dea0e78b026690a67fe7fe",
|
||||
"reference": "978261d3c4ffd64f99dea0e78b026690a67fe7fe",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5",
|
||||
"symfony/polyfill-intl-grapheme": "~1.0",
|
||||
"symfony/polyfill-intl-normalizer": "~1.0",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/translation-contracts": "^1.1|^2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\String\\": ""
|
||||
},
|
||||
"files": [
|
||||
"Resources/functions.php"
|
||||
],
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony String component",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"grapheme",
|
||||
"i18n",
|
||||
"string",
|
||||
"unicode",
|
||||
"utf-8",
|
||||
"utf8"
|
||||
],
|
||||
"time": "2020-01-14T17:54:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/swiftmailer-bundle",
|
||||
"version": "v3.4.0",
|
||||
@@ -8750,41 +8937,41 @@
|
||||
"time": "2020-01-21T11:12:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/extensions",
|
||||
"version": "v1.5.4",
|
||||
"name": "twig/extra-bundle",
|
||||
"version": "v3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig-extensions.git",
|
||||
"reference": "57873c8b0c1be51caa47df2cdb824490beb16202"
|
||||
"url": "https://github.com/twigphp/twig-extra-bundle.git",
|
||||
"reference": "ce5c97dd566d9acd5d1fbd5eb76b6d264614725a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig-extensions/zipball/57873c8b0c1be51caa47df2cdb824490beb16202",
|
||||
"reference": "57873c8b0c1be51caa47df2cdb824490beb16202",
|
||||
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/ce5c97dd566d9acd5d1fbd5eb76b6d264614725a",
|
||||
"reference": "ce5c97dd566d9acd5d1fbd5eb76b6d264614725a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"twig/twig": "^1.27|^2.0"
|
||||
"php": "^7.1.3",
|
||||
"symfony/framework-bundle": "^4.3|^5.0",
|
||||
"symfony/twig-bundle": "^4.3|^5.0",
|
||||
"twig/twig": "^2.4|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^3.4",
|
||||
"symfony/translation": "^2.7|^3.4"
|
||||
"twig/cssinliner-extra": "^2.12|^3.0",
|
||||
"twig/html-extra": "^2.12|^3.0",
|
||||
"twig/inky-extra": "^2.12|^3.0",
|
||||
"twig/intl-extra": "^2.12|^3.0",
|
||||
"twig/markdown-extra": "^2.12|^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/translation": "Allow the time_diff output to be translated"
|
||||
},
|
||||
"type": "library",
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.5-dev"
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Twig_Extensions_": "lib/"
|
||||
},
|
||||
"psr-4": {
|
||||
"Twig\\Extensions\\": "src/"
|
||||
"Twig\\Extra\\TwigExtraBundle\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
@@ -8794,15 +8981,127 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
}
|
||||
],
|
||||
"description": "Common additional features for Twig that do not directly belong in core",
|
||||
"description": "A Symfony bundle for extra Twig extensions",
|
||||
"homepage": "https://twig.symfony.com",
|
||||
"keywords": [
|
||||
"i18n",
|
||||
"text"
|
||||
"bundle",
|
||||
"extra",
|
||||
"twig"
|
||||
],
|
||||
"time": "2018-12-05T18:34:18+00:00"
|
||||
"time": "2019-12-28T07:09:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/intl-extra",
|
||||
"version": "v3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/intl-extra.git",
|
||||
"reference": "291d79ef98891da3efe14f0771fbe03a25fe6bec"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/291d79ef98891da3efe14f0771fbe03a25fe6bec",
|
||||
"reference": "291d79ef98891da3efe14f0771fbe03a25fe6bec",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"symfony/intl": "^4.3|^5.0",
|
||||
"twig/twig": "^2.4|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^4.4|^5.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Twig\\Extra\\Intl\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
}
|
||||
],
|
||||
"description": "A Twig extension for Intl",
|
||||
"homepage": "https://twig.symfony.com",
|
||||
"keywords": [
|
||||
"intl",
|
||||
"twig"
|
||||
],
|
||||
"time": "2019-12-28T07:09:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/string-extra",
|
||||
"version": "v3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/string-extra.git",
|
||||
"reference": "54fb5511c4a0189de1d48d0f7c98843c270084e8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/string-extra/zipball/54fb5511c4a0189de1d48d0f7c98843c270084e8",
|
||||
"reference": "54fb5511c4a0189de1d48d0f7c98843c270084e8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.9",
|
||||
"symfony/string": "^5.0",
|
||||
"twig/twig": "^2.4|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^4.4|^5.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.12-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Twig\\Extra\\String\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
}
|
||||
],
|
||||
"description": "A Twig extension for Symfony String",
|
||||
"homepage": "https://twig.symfony.com",
|
||||
"keywords": [
|
||||
"html",
|
||||
"string",
|
||||
"twig",
|
||||
"unicode"
|
||||
],
|
||||
"time": "2019-12-27T07:33:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
@@ -11746,6 +12045,6 @@
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-overrides": {
|
||||
"php": "7.2"
|
||||
"php": "7.2.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,5 @@ return [
|
||||
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
services:
|
||||
_defaults:
|
||||
public: false
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
#Twig\Extensions\ArrayExtension: ~
|
||||
#Twig\Extensions\DateExtension: ~
|
||||
Twig\Extensions\IntlExtension: ~
|
||||
#Twig\Extensions\TextExtension: ~
|
||||
Twig\Extensions\StringLoader:
|
||||
class: Twig\Extension\StringLoaderExtension
|
||||
@@ -9,44 +9,37 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
class InvoiceDocument
|
||||
final class InvoiceDocument
|
||||
{
|
||||
/**
|
||||
* @var \SplFileInfo
|
||||
*/
|
||||
private $file;
|
||||
|
||||
/**
|
||||
* @param \SplFileInfo $file
|
||||
*/
|
||||
public function __construct(\SplFileInfo $file)
|
||||
{
|
||||
$this->file = $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): string
|
||||
{
|
||||
$file = $this->file->getFilename();
|
||||
|
||||
return substr($file, 0, strpos($file, '.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): string
|
||||
{
|
||||
return basename($this->getFilename());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFilename(): string
|
||||
{
|
||||
return $this->file->getRealPath();
|
||||
}
|
||||
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return $this->file->getExtension();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,25 @@ class InvoiceTemplate
|
||||
*/
|
||||
private $paymentDetails;
|
||||
|
||||
/**
|
||||
* Used when rendering HTML templates.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="decimal_duration", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $decimalDuration = false;
|
||||
|
||||
/**
|
||||
* Used when rendering HTML templates.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="language", type="string", length=6, nullable=true)
|
||||
*/
|
||||
private $language;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -295,6 +314,30 @@ class InvoiceTemplate
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDecimalDuration(): bool
|
||||
{
|
||||
return $this->decimalDuration;
|
||||
}
|
||||
|
||||
public function setDecimalDuration(bool $decimalDuration): InvoiceTemplate
|
||||
{
|
||||
$this->decimalDuration = $decimalDuration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLanguage(): ?string
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
public function setLanguage(?string $language): InvoiceTemplate
|
||||
{
|
||||
$this->language = $language;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,8 @@ use App\Entity\InvoiceTemplate;
|
||||
use App\Form\Type\InvoiceCalculatorType;
|
||||
use App\Form\Type\InvoiceNumberGeneratorType;
|
||||
use App\Form\Type\InvoiceRendererType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
@@ -70,6 +72,12 @@ class InvoiceTemplateForm extends AbstractType
|
||||
->add('renderer', InvoiceRendererType::class, [])
|
||||
->add('calculator', InvoiceCalculatorType::class, [])
|
||||
->add('numberGenerator', InvoiceNumberGeneratorType::class, [])
|
||||
->add('language', LanguageType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('decimalDuration', YesNoType::class, [
|
||||
'label' => 'label.decimalDuration'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
135
src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
Normal file
135
src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\InvoiceItem;
|
||||
use App\Invoice\InvoiceItemHydrator;
|
||||
use App\Invoice\InvoiceModel;
|
||||
|
||||
class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
||||
{
|
||||
/**
|
||||
* @var InvoiceModel
|
||||
*/
|
||||
private $model;
|
||||
|
||||
public function setInvoiceModel(InvoiceModel $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
public function hydrate(InvoiceItem $item): array
|
||||
{
|
||||
$formatter = $this->model->getFormatter();
|
||||
|
||||
$rate = $item->getRate();
|
||||
$appliedRate = $item->getHourlyRate();
|
||||
$amount = $formatter->getFormattedDuration($item->getDuration());
|
||||
$description = $item->getDescription();
|
||||
|
||||
if ($item->isFixedRate()) {
|
||||
$appliedRate = $item->getFixedRate();
|
||||
$amount = $item->getAmount();
|
||||
}
|
||||
|
||||
$activity = $item->getActivity();
|
||||
$project = $item->getProject();
|
||||
$customer = $project->getCustomer();
|
||||
$currency = $customer->getCurrency();
|
||||
$user = $item->getUser();
|
||||
$begin = $item->getBegin();
|
||||
$end = $item->getEnd();
|
||||
|
||||
if (empty($description) && null !== $activity) {
|
||||
$description = $activity->getName();
|
||||
}
|
||||
|
||||
// this should never happen!
|
||||
if (empty($appliedRate)) {
|
||||
$appliedRate = 0;
|
||||
}
|
||||
|
||||
$values = [
|
||||
'entry.row' => '',
|
||||
'entry.description' => $description,
|
||||
'entry.amount' => $amount,
|
||||
'entry.type' => $item->getType(),
|
||||
'entry.category' => $item->getCategory(),
|
||||
'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency),
|
||||
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, null),
|
||||
'entry.rate_plain' => $appliedRate,
|
||||
'entry.total' => $formatter->getFormattedMoney($rate, $currency),
|
||||
'entry.total_nc' => $formatter->getFormattedMoney($rate, null),
|
||||
'entry.total_plain' => $rate,
|
||||
'entry.currency' => $currency,
|
||||
'entry.duration' => $item->getDuration(),
|
||||
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($item->getDuration()),
|
||||
'entry.duration_minutes' => number_format($item->getDuration() / 60),
|
||||
'entry.begin' => $formatter->getFormattedDateTime($begin),
|
||||
'entry.begin_time' => $formatter->getFormattedTime($begin),
|
||||
'entry.begin_timestamp' => $begin->getTimestamp(),
|
||||
'entry.end' => $formatter->getFormattedDateTime($end),
|
||||
'entry.end_time' => $formatter->getFormattedTime($end),
|
||||
'entry.end_timestamp' => $end->getTimestamp(),
|
||||
'entry.date' => $formatter->getFormattedDateTime($begin),
|
||||
'entry.user_id' => $user->getId(),
|
||||
'entry.user_name' => $user->getUsername(),
|
||||
'entry.user_title' => $user->getTitle(),
|
||||
'entry.user_alias' => $user->getAlias(),
|
||||
];
|
||||
|
||||
if (null !== $activity) {
|
||||
$values = array_merge($values, [
|
||||
'entry.activity' => $activity->getName(),
|
||||
'entry.activity_id' => $activity->getId(),
|
||||
]);
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'entry.activity.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $project) {
|
||||
$values = array_merge($values, [
|
||||
'entry.project' => $project->getName(),
|
||||
'entry.project_id' => $project->getId(),
|
||||
]);
|
||||
|
||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'entry.project.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $customer) {
|
||||
$values = array_merge($values, [
|
||||
'entry.customer' => $customer->getName(),
|
||||
'entry.customer_id' => $customer->getId(),
|
||||
]);
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'entry.customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($item->getAdditionalFields() as $name => $value) {
|
||||
$values = array_merge($values, [
|
||||
'entry.meta.' . $name => $value,
|
||||
]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
48
src/Invoice/Hydrator/InvoiceModelActivityHydrator.php
Normal file
48
src/Invoice/Hydrator/InvoiceModelActivityHydrator.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceModelHydrator;
|
||||
|
||||
class InvoiceModelActivityHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
public function hydrate(InvoiceModel $model): array
|
||||
{
|
||||
$activity = $model->getQuery()->getActivity();
|
||||
|
||||
if (null === $activity) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$formatter = $model->getFormatter();
|
||||
$currency = $model->getCurrency();
|
||||
|
||||
$values = [
|
||||
'activity.id' => $activity->getId(),
|
||||
'activity.name' => $activity->getName(),
|
||||
'activity.comment' => $activity->getComment(),
|
||||
'activity.fixed_rate' => $formatter->getFormattedMoney($activity->getFixedRate(), $currency),
|
||||
'activity.fixed_rate_nc' => $formatter->getFormattedMoney($activity->getFixedRate(), null),
|
||||
'activity.fixed_rate_plain' => $activity->getFixedRate(),
|
||||
'activity.hourly_rate' => $formatter->getFormattedMoney($activity->getHourlyRate(), $currency),
|
||||
'activity.hourly_rate_nc' => $formatter->getFormattedMoney($activity->getHourlyRate(), null),
|
||||
'activity.hourly_rate_plain' => $activity->getHourlyRate(),
|
||||
];
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'activity.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
55
src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php
Normal file
55
src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceModelHydrator;
|
||||
|
||||
class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
public function hydrate(InvoiceModel $model): array
|
||||
{
|
||||
$customer = $model->getCustomer();
|
||||
|
||||
if (null === $customer) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$formatter = $model->getFormatter();
|
||||
$currency = $model->getCurrency();
|
||||
|
||||
$values = [
|
||||
'customer.id' => $customer->getId(),
|
||||
'customer.address' => $customer->getAddress(),
|
||||
'customer.name' => $customer->getName(),
|
||||
'customer.contact' => $customer->getContact(),
|
||||
'customer.company' => $customer->getCompany(),
|
||||
'customer.vat' => $customer->getVatId(),
|
||||
'customer.number' => $customer->getNumber(),
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
'customer.comment' => $customer->getComment(),
|
||||
'customer.fixed_rate' => $formatter->getFormattedMoney($customer->getFixedRate(), $currency),
|
||||
'customer.fixed_rate_nc' => $formatter->getFormattedMoney($customer->getFixedRate(), null),
|
||||
'customer.fixed_rate_plain' => $customer->getFixedRate(),
|
||||
'customer.hourly_rate' => $formatter->getFormattedMoney($customer->getHourlyRate(), $currency),
|
||||
'customer.hourly_rate_nc' => $formatter->getFormattedMoney($customer->getHourlyRate(), null),
|
||||
'customer.hourly_rate_plain' => $customer->getHourlyRate(),
|
||||
];
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
64
src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php
Normal file
64
src/Invoice/Hydrator/InvoiceModelDefaultHydrator.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\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceModelHydrator;
|
||||
|
||||
class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
public function hydrate(InvoiceModel $model): array
|
||||
{
|
||||
$currency = $model->getCurrency();
|
||||
$tax = $model->getCalculator()->getTax();
|
||||
$total = $model->getCalculator()->getTotal();
|
||||
$subtotal = $model->getCalculator()->getSubtotal();
|
||||
$formatter = $model->getFormatter();
|
||||
|
||||
$values = [
|
||||
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
|
||||
'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()),
|
||||
'invoice.number' => $model->getNumberGenerator()->getInvoiceNumber(),
|
||||
'invoice.currency' => $currency,
|
||||
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
|
||||
'invoice.vat' => $model->getCalculator()->getVat(),
|
||||
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency),
|
||||
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, null),
|
||||
'invoice.tax_plain' => $tax,
|
||||
'invoice.total_time' => $formatter->getFormattedDuration($model->getCalculator()->getTimeWorked()),
|
||||
'invoice.duration_decimal' => $formatter->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()),
|
||||
'invoice.total' => $formatter->getFormattedMoney($total, $currency),
|
||||
'invoice.total_nc' => $formatter->getFormattedMoney($total, null),
|
||||
'invoice.total_plain' => $total,
|
||||
'invoice.subtotal' => $formatter->getFormattedMoney($subtotal, $currency),
|
||||
'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, null),
|
||||
'invoice.subtotal_plain' => $subtotal,
|
||||
|
||||
'template.name' => $model->getTemplate()->getName(),
|
||||
'template.company' => $model->getTemplate()->getCompany(),
|
||||
'template.address' => $model->getTemplate()->getAddress(),
|
||||
'template.title' => $model->getTemplate()->getTitle(),
|
||||
'template.payment_terms' => $model->getTemplate()->getPaymentTerms(),
|
||||
'template.due_days' => $model->getTemplate()->getDueDays(),
|
||||
'template.vat_id' => $model->getTemplate()->getVatId(),
|
||||
'template.contact' => $model->getTemplate()->getContact(),
|
||||
'template.payment_details' => $model->getTemplate()->getPaymentDetails(),
|
||||
|
||||
'query.begin' => $formatter->getFormattedDateTime($model->getQuery()->getBegin()),
|
||||
'query.day' => $model->getQuery()->getBegin()->format('d'),
|
||||
'query.end' => $formatter->getFormattedDateTime($model->getQuery()->getEnd()),
|
||||
'query.month' => $formatter->getFormattedMonthName($model->getQuery()->getBegin()),
|
||||
'query.month_number' => $model->getQuery()->getBegin()->format('m'),
|
||||
'query.year' => $model->getQuery()->getBegin()->format('Y'),
|
||||
];
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
58
src/Invoice/Hydrator/InvoiceModelProjectHydrator.php
Normal file
58
src/Invoice/Hydrator/InvoiceModelProjectHydrator.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\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceModelHydrator;
|
||||
|
||||
class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
public function hydrate(InvoiceModel $model): array
|
||||
{
|
||||
$project = $model->getQuery()->getProject();
|
||||
|
||||
if (null === $project) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$formatter = $model->getFormatter();
|
||||
$currency = $model->getCurrency();
|
||||
|
||||
$values = [
|
||||
'project.id' => $project->getId(),
|
||||
'project.name' => $project->getName(),
|
||||
'project.comment' => $project->getComment(),
|
||||
'project.order_number' => $project->getOrderNumber(),
|
||||
'project.start_date' => null !== $project->getStart() ? $formatter->getFormattedDateTime($project->getStart()) : '',
|
||||
'project.end_date' => null !== $project->getEnd() ? $formatter->getFormattedDateTime($project->getEnd()) : '',
|
||||
'project.order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '',
|
||||
'project.fixed_rate' => $formatter->getFormattedMoney($project->getFixedRate(), $currency),
|
||||
'project.fixed_rate_nc' => $formatter->getFormattedMoney($project->getFixedRate(), null),
|
||||
'project.fixed_rate_plain' => $project->getFixedRate(),
|
||||
'project.hourly_rate' => $formatter->getFormattedMoney($project->getHourlyRate(), $currency),
|
||||
'project.hourly_rate_nc' => $formatter->getFormattedMoney($project->getHourlyRate(), null),
|
||||
'project.hourly_rate_plain' => $project->getHourlyRate(),
|
||||
'project.budget_money' => $formatter->getFormattedMoney($project->getBudget(), $currency),
|
||||
'project.budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), null),
|
||||
'project.budget_money_plain' => $project->getBudget(),
|
||||
'project.budget_time' => $project->getTimeBudget(),
|
||||
'project.budget_time_decimal' => $formatter->getFormattedDecimalDuration($project->getTimeBudget()),
|
||||
'project.budget_time_minutes' => number_format($project->getTimeBudget() / 60),
|
||||
];
|
||||
|
||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'project.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
42
src/Invoice/Hydrator/InvoiceModelUserHydrator.php
Normal file
42
src/Invoice/Hydrator/InvoiceModelUserHydrator.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice\Hydrator;
|
||||
|
||||
use App\Entity\UserPreference;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceModelHydrator;
|
||||
|
||||
class InvoiceModelUserHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
public function hydrate(InvoiceModel $model): array
|
||||
{
|
||||
$user = $model->getUser();
|
||||
|
||||
if (null === $user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$values = [
|
||||
'user.name' => $user->getUsername(),
|
||||
'user.email' => $user->getEmail(),
|
||||
'user.title' => $user->getTitle(),
|
||||
'user.alias' => $user->getAlias(),
|
||||
];
|
||||
|
||||
/** @var UserPreference $metaField */
|
||||
foreach ($user->getPreferences() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'user.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
17
src/Invoice/InvoiceItemHydrator.php
Normal file
17
src/Invoice/InvoiceItemHydrator.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
interface InvoiceItemHydrator
|
||||
{
|
||||
public function setInvoiceModel(InvoiceModel $model);
|
||||
|
||||
public function hydrate(InvoiceItem $item): array;
|
||||
}
|
||||
@@ -12,7 +12,12 @@ namespace App\Invoice;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Invoice\Hydrator\InvoiceItemDefaultHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelActivityHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelCustomerHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelDefaultHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelProjectHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelUserHydrator;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
|
||||
/**
|
||||
@@ -57,11 +62,25 @@ final class InvoiceModel
|
||||
* @var InvoiceFormatter
|
||||
*/
|
||||
private $formatter;
|
||||
/**
|
||||
* @var InvoiceModelHydrator[]
|
||||
*/
|
||||
private $modelHydrator = [];
|
||||
/**
|
||||
* @var InvoiceItemHydrator[]
|
||||
*/
|
||||
private $itemHydrator = [];
|
||||
|
||||
public function __construct(InvoiceFormatter $formatter)
|
||||
{
|
||||
$this->invoiceDate = new \DateTime();
|
||||
$this->formatter = $formatter;
|
||||
$this->addModelHydrator(new InvoiceModelDefaultHydrator());
|
||||
$this->addModelHydrator(new InvoiceModelCustomerHydrator());
|
||||
$this->addModelHydrator(new InvoiceModelProjectHydrator());
|
||||
$this->addModelHydrator(new InvoiceModelActivityHydrator());
|
||||
$this->addModelHydrator(new InvoiceModelUserHydrator());
|
||||
$this->addItemHydrator(new InvoiceItemDefaultHydrator());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +137,22 @@ final class InvoiceModel
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addModelHydrator(InvoiceModelHydrator $hydrator): InvoiceModel
|
||||
{
|
||||
$this->modelHydrator[] = $hydrator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addItemHydrator(InvoiceItemHydrator $hydrator): InvoiceModel
|
||||
{
|
||||
$hydrator->setInvoiceModel($this);
|
||||
|
||||
$this->itemHydrator[] = $hydrator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTemplate(): ?InvoiceTemplate
|
||||
{
|
||||
return $this->template;
|
||||
@@ -136,7 +171,7 @@ final class InvoiceModel
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Customer|null $customer
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setCustomer($customer): InvoiceModel
|
||||
@@ -223,146 +258,10 @@ final class InvoiceModel
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$model = $this;
|
||||
$customer = $model->getCustomer();
|
||||
$project = $model->getQuery()->getProject();
|
||||
$activity = $model->getQuery()->getActivity();
|
||||
$currency = $this->getCurrency();
|
||||
$tax = $model->getCalculator()->getTax();
|
||||
$total = $model->getCalculator()->getTotal();
|
||||
$subtotal = $model->getCalculator()->getSubtotal();
|
||||
$formatter = $model->getFormatter();
|
||||
$values = [];
|
||||
|
||||
$values = [
|
||||
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
|
||||
'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()),
|
||||
'invoice.number' => $model->getNumberGenerator()->getInvoiceNumber(),
|
||||
'invoice.currency' => $currency,
|
||||
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
|
||||
'invoice.vat' => $model->getCalculator()->getVat(),
|
||||
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency),
|
||||
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, null),
|
||||
'invoice.tax_plain' => $tax,
|
||||
'invoice.total_time' => $formatter->getFormattedDuration($model->getCalculator()->getTimeWorked()),
|
||||
'invoice.duration_decimal' => $formatter->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()),
|
||||
'invoice.total' => $formatter->getFormattedMoney($total, $currency),
|
||||
'invoice.total_nc' => $formatter->getFormattedMoney($total, null),
|
||||
'invoice.total_plain' => $total,
|
||||
'invoice.subtotal' => $formatter->getFormattedMoney($subtotal, $currency),
|
||||
'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, null),
|
||||
'invoice.subtotal_plain' => $subtotal,
|
||||
|
||||
'template.name' => $model->getTemplate()->getName(),
|
||||
'template.company' => $model->getTemplate()->getCompany(),
|
||||
'template.address' => $model->getTemplate()->getAddress(),
|
||||
'template.title' => $model->getTemplate()->getTitle(),
|
||||
'template.payment_terms' => $model->getTemplate()->getPaymentTerms(),
|
||||
'template.due_days' => $model->getTemplate()->getDueDays(),
|
||||
'template.vat_id' => $model->getTemplate()->getVatId(),
|
||||
'template.contact' => $model->getTemplate()->getContact(),
|
||||
'template.payment_details' => $model->getTemplate()->getPaymentDetails(),
|
||||
|
||||
'query.begin' => $formatter->getFormattedDateTime($model->getQuery()->getBegin()),
|
||||
'query.day' => $model->getQuery()->getBegin()->format('d'),
|
||||
'query.end' => $formatter->getFormattedDateTime($model->getQuery()->getEnd()),
|
||||
'query.month' => $formatter->getFormattedMonthName($model->getQuery()->getBegin()),
|
||||
'query.month_number' => $model->getQuery()->getBegin()->format('m'),
|
||||
'query.year' => $model->getQuery()->getBegin()->format('Y'),
|
||||
];
|
||||
|
||||
if (null !== $model->getUser()) {
|
||||
$user = $model->getUser();
|
||||
|
||||
/** @var UserPreference $metaField */
|
||||
foreach ($user->getPreferences() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'user.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
|
||||
$values = array_merge($values, [
|
||||
'user.name' => $user->getUsername(),
|
||||
'user.email' => $user->getEmail(),
|
||||
'user.title' => $user->getTitle(),
|
||||
'user.alias' => $user->getAlias(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (null !== $activity) {
|
||||
$values = array_merge($values, [
|
||||
'activity.id' => $activity->getId(),
|
||||
'activity.name' => $activity->getName(),
|
||||
'activity.comment' => $activity->getComment(),
|
||||
'activity.fixed_rate' => $formatter->getFormattedMoney($activity->getFixedRate(), $currency),
|
||||
'activity.fixed_rate_nc' => $formatter->getFormattedMoney($activity->getFixedRate(), null),
|
||||
'activity.fixed_rate_plain' => $activity->getFixedRate(),
|
||||
'activity.hourly_rate' => $formatter->getFormattedMoney($activity->getHourlyRate(), $currency),
|
||||
'activity.hourly_rate_nc' => $formatter->getFormattedMoney($activity->getHourlyRate(), null),
|
||||
'activity.hourly_rate_plain' => $activity->getHourlyRate(),
|
||||
]);
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'activity.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $project) {
|
||||
$values = array_merge($values, [
|
||||
'project.id' => $project->getId(),
|
||||
'project.name' => $project->getName(),
|
||||
'project.comment' => $project->getComment(),
|
||||
'project.order_number' => $project->getOrderNumber(),
|
||||
'project.start_date' => null !== $project->getStart() ? $formatter->getFormattedDateTime($project->getStart()) : '',
|
||||
'project.end_date' => null !== $project->getEnd() ? $formatter->getFormattedDateTime($project->getEnd()) : '',
|
||||
'project.order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '',
|
||||
'project.fixed_rate' => $formatter->getFormattedMoney($project->getFixedRate(), $currency),
|
||||
'project.fixed_rate_nc' => $formatter->getFormattedMoney($project->getFixedRate(), null),
|
||||
'project.fixed_rate_plain' => $project->getFixedRate(),
|
||||
'project.hourly_rate' => $formatter->getFormattedMoney($project->getHourlyRate(), $currency),
|
||||
'project.hourly_rate_nc' => $formatter->getFormattedMoney($project->getHourlyRate(), null),
|
||||
'project.hourly_rate_plain' => $project->getHourlyRate(),
|
||||
'project.budget_money' => $formatter->getFormattedMoney($project->getBudget(), $currency),
|
||||
'project.budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), null),
|
||||
'project.budget_money_plain' => $project->getBudget(),
|
||||
'project.budget_time' => $project->getTimeBudget(),
|
||||
'project.budget_time_decimal' => $formatter->getFormattedDecimalDuration($project->getTimeBudget()),
|
||||
'project.budget_time_minutes' => number_format($project->getTimeBudget() / 60),
|
||||
]);
|
||||
|
||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'project.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $customer) {
|
||||
$values = array_merge($values, [
|
||||
'customer.id' => $customer->getId(),
|
||||
'customer.address' => $customer->getAddress(),
|
||||
'customer.name' => $customer->getName(),
|
||||
'customer.contact' => $customer->getContact(),
|
||||
'customer.company' => $customer->getCompany(),
|
||||
'customer.vat' => $customer->getVatId(),
|
||||
'customer.number' => $customer->getNumber(),
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
'customer.comment' => $customer->getComment(),
|
||||
'customer.fixed_rate' => $formatter->getFormattedMoney($customer->getFixedRate(), $currency),
|
||||
'customer.fixed_rate_nc' => $formatter->getFormattedMoney($customer->getFixedRate(), null),
|
||||
'customer.fixed_rate_plain' => $customer->getFixedRate(),
|
||||
'customer.hourly_rate' => $formatter->getFormattedMoney($customer->getHourlyRate(), $currency),
|
||||
'customer.hourly_rate_nc' => $formatter->getFormattedMoney($customer->getHourlyRate(), null),
|
||||
'customer.hourly_rate_plain' => $customer->getHourlyRate(),
|
||||
]);
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
foreach ($this->modelHydrator as $hydrator) {
|
||||
$values = array_merge($values, $hydrator->hydrate($this));
|
||||
}
|
||||
|
||||
return $values;
|
||||
@@ -370,107 +269,10 @@ final class InvoiceModel
|
||||
|
||||
public function itemToArray(InvoiceItem $invoiceItem): array
|
||||
{
|
||||
$formatter = $this->getFormatter();
|
||||
$values = [];
|
||||
|
||||
$rate = $invoiceItem->getRate();
|
||||
$appliedRate = $invoiceItem->getHourlyRate();
|
||||
$amount = $formatter->getFormattedDuration($invoiceItem->getDuration());
|
||||
$description = $invoiceItem->getDescription();
|
||||
|
||||
if ($invoiceItem->isFixedRate()) {
|
||||
$appliedRate = $invoiceItem->getFixedRate();
|
||||
$amount = $invoiceItem->getAmount();
|
||||
}
|
||||
|
||||
$activity = $invoiceItem->getActivity();
|
||||
$project = $invoiceItem->getProject();
|
||||
$customer = $project->getCustomer();
|
||||
$currency = $customer->getCurrency();
|
||||
$user = $invoiceItem->getUser();
|
||||
$begin = $invoiceItem->getBegin();
|
||||
$end = $invoiceItem->getEnd();
|
||||
|
||||
if (empty($description) && null !== $activity) {
|
||||
$description = $activity->getName();
|
||||
}
|
||||
|
||||
// this should never happen!
|
||||
if (empty($appliedRate)) {
|
||||
$appliedRate = 0;
|
||||
}
|
||||
|
||||
$values = [
|
||||
'entry.row' => '',
|
||||
'entry.description' => $description,
|
||||
'entry.amount' => $amount,
|
||||
'entry.type' => $invoiceItem->getType(),
|
||||
'entry.category' => $invoiceItem->getCategory(),
|
||||
'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency),
|
||||
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, null),
|
||||
'entry.rate_plain' => $appliedRate,
|
||||
'entry.total' => $formatter->getFormattedMoney($rate, $currency),
|
||||
'entry.total_nc' => $formatter->getFormattedMoney($rate, null),
|
||||
'entry.total_plain' => $rate,
|
||||
'entry.currency' => $currency,
|
||||
'entry.duration' => $invoiceItem->getDuration(),
|
||||
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($invoiceItem->getDuration()),
|
||||
'entry.duration_minutes' => number_format($invoiceItem->getDuration() / 60),
|
||||
'entry.begin' => $formatter->getFormattedDateTime($begin),
|
||||
'entry.begin_time' => $formatter->getFormattedTime($begin),
|
||||
'entry.begin_timestamp' => $begin->getTimestamp(),
|
||||
'entry.end' => $formatter->getFormattedDateTime($end),
|
||||
'entry.end_time' => $formatter->getFormattedTime($end),
|
||||
'entry.end_timestamp' => $end->getTimestamp(),
|
||||
'entry.date' => $formatter->getFormattedDateTime($begin),
|
||||
'entry.user_id' => $user->getId(),
|
||||
'entry.user_name' => $user->getUsername(),
|
||||
'entry.user_title' => $user->getTitle(),
|
||||
'entry.user_alias' => $user->getAlias(),
|
||||
];
|
||||
|
||||
if (null !== $activity) {
|
||||
$values = array_merge($values, [
|
||||
'entry.activity' => $activity->getName(),
|
||||
'entry.activity_id' => $activity->getId(),
|
||||
]);
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'entry.activity.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $project) {
|
||||
$values = array_merge($values, [
|
||||
'entry.project' => $project->getName(),
|
||||
'entry.project_id' => $project->getId(),
|
||||
]);
|
||||
|
||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'entry.project.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $customer) {
|
||||
$values = array_merge($values, [
|
||||
'entry.customer' => $customer->getName(),
|
||||
'entry.customer_id' => $customer->getId(),
|
||||
]);
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'entry.customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($invoiceItem->getAdditionalFields() as $name => $value) {
|
||||
$values = array_merge($values, [
|
||||
'entry.meta.' . $name => $value,
|
||||
]);
|
||||
foreach ($this->itemHydrator as $hydrator) {
|
||||
$values = array_merge($values, $hydrator->hydrate($invoiceItem));
|
||||
}
|
||||
|
||||
return $values;
|
||||
|
||||
15
src/Invoice/InvoiceModelHydrator.php
Normal file
15
src/Invoice/InvoiceModelHydrator.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
interface InvoiceModelHydrator
|
||||
{
|
||||
public function hydrate(InvoiceModel $model): array;
|
||||
}
|
||||
@@ -10,8 +10,10 @@
|
||||
namespace App\Invoice\Renderer;
|
||||
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -43,6 +45,23 @@ abstract class AbstractRenderer
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function buildFilename(InvoiceModel $model): string
|
||||
{
|
||||
$filename = $model->getNumberGenerator()->getInvoiceNumber();
|
||||
|
||||
$company = $model->getCustomer()->getCompany();
|
||||
if (empty($company)) {
|
||||
$company = $model->getCustomer()->getName();
|
||||
}
|
||||
|
||||
if (!empty($company)) {
|
||||
$company = new UnicodeString($company);
|
||||
$filename .= '-' . $company->snake();
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $file
|
||||
* @param string $filename
|
||||
|
||||
@@ -99,8 +99,9 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
|
||||
}
|
||||
|
||||
$filename = $this->saveSpreadsheet($spreadsheet);
|
||||
$userFilename = $this->buildFilename($model) . '.' . $document->getFileExtension();
|
||||
|
||||
return $this->getFileResponse($filename, basename($document->getFilename()));
|
||||
return $this->getFileResponse($filename, $userFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,8 +30,6 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface
|
||||
{
|
||||
Settings::setOutputEscapingEnabled(false);
|
||||
|
||||
$filename = basename($document->getFilename());
|
||||
|
||||
$xmlEscaper = new Xml();
|
||||
$template = new TemplateProcessor($document->getFilename());
|
||||
|
||||
@@ -70,6 +68,8 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface
|
||||
|
||||
clearstatcache(true, $cacheFile);
|
||||
|
||||
$filename = $this->buildFilename($model) . '.' . $document->getFileExtension();
|
||||
|
||||
return $this->getFileResponse(new Stream($cacheFile), $filename);
|
||||
}
|
||||
|
||||
|
||||
52
src/Migrations/Version20200204124425.php
Normal file
52
src/Migrations/Version20200204124425.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
final class Version20200204124425 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Adds language and decimal_duration column to invoice template table';
|
||||
}
|
||||
|
||||
protected function isSupportingForeignKeys(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
if ($this->isPlatformSqlite()) {
|
||||
// does fail if we use transactions, as tables are re-created and foreign keys would fail
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$invoiceTemplates = $schema->getTable('kimai2_invoice_templates');
|
||||
$invoiceTemplates->addColumn('decimal_duration', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$invoiceTemplates->addColumn('language', 'string', ['notnull' => false, 'length' => 6]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$invoiceTemplates = $schema->getTable('kimai2_invoice_templates');
|
||||
$invoiceTemplates->dropColumn('language');
|
||||
$invoiceTemplates->dropColumn('decimal_duration');
|
||||
}
|
||||
}
|
||||
@@ -102,17 +102,18 @@ class Extensions extends AbstractExtension
|
||||
* Transforms seconds into a duration string.
|
||||
*
|
||||
* @param int|Timesheet $duration
|
||||
* @param string $format
|
||||
* @param bool $decimal
|
||||
* @return string
|
||||
*/
|
||||
public function duration($duration, $format = null)
|
||||
public function duration($duration, $decimal = false)
|
||||
{
|
||||
$duration = $this->getSecondsForDuration($duration);
|
||||
|
||||
if (null === $format) {
|
||||
$format = $this->localeSettings->getDurationFormat();
|
||||
if ($decimal) {
|
||||
return $this->durationDecimal($duration);
|
||||
}
|
||||
|
||||
$duration = $this->getSecondsForDuration($duration);
|
||||
$format = $this->localeSettings->getDurationFormat();
|
||||
|
||||
return $this->formatDuration($duration, $format);
|
||||
}
|
||||
|
||||
|
||||
27
symfony.lock
27
symfony.lock
@@ -286,7 +286,7 @@
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"php": {
|
||||
"version": "7.2"
|
||||
"version": "7.2.9"
|
||||
},
|
||||
"php-cs-fixer/diff": {
|
||||
"version": "v1.2.0"
|
||||
@@ -567,12 +567,18 @@
|
||||
"symfony/polyfill-ctype": {
|
||||
"version": "v1.8.0"
|
||||
},
|
||||
"symfony/polyfill-intl-grapheme": {
|
||||
"version": "v1.13.1"
|
||||
},
|
||||
"symfony/polyfill-intl-icu": {
|
||||
"version": "v1.6.0"
|
||||
},
|
||||
"symfony/polyfill-intl-idn": {
|
||||
"version": "v1.11.0"
|
||||
},
|
||||
"symfony/polyfill-intl-normalizer": {
|
||||
"version": "v1.13.1"
|
||||
},
|
||||
"symfony/polyfill-mbstring": {
|
||||
"version": "v1.6.0"
|
||||
},
|
||||
@@ -633,6 +639,9 @@
|
||||
"symfony/stopwatch": {
|
||||
"version": "v4.0.3"
|
||||
},
|
||||
"symfony/string": {
|
||||
"version": "v5.0.4"
|
||||
},
|
||||
"symfony/swiftmailer-bundle": {
|
||||
"version": "2.5",
|
||||
"recipe": {
|
||||
@@ -711,14 +720,14 @@
|
||||
"theseer/tokenizer": {
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"twig/extensions": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "1.0",
|
||||
"ref": "4851df0afc426b8f07204379d21fca25b6df5d68"
|
||||
}
|
||||
"twig/extra-bundle": {
|
||||
"version": "v3.0.1"
|
||||
},
|
||||
"twig/intl-extra": {
|
||||
"version": "v3.0.1"
|
||||
},
|
||||
"twig/string-extra": {
|
||||
"version": "v3.0.1"
|
||||
},
|
||||
"twig/twig": {
|
||||
"version": "v2.4.4"
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<!-- Page rendered on {{ 'now'|localizeddate('long', 'long', null, 'UTC') }} -->
|
||||
<!-- Page rendered on {{ 'now'|date_full }} -->
|
||||
{% endblock %}
|
||||
|
||||
{% block navbar_start %}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{% if form_theme is defined %}
|
||||
{% form_theme form form_theme %}
|
||||
{% endif %}
|
||||
<div class="box box-primary">
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'boxtype': 'primary'} %}
|
||||
{% block box_before %}
|
||||
{% block form_before %}{% endblock %}
|
||||
{{ form_start(form) }}
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">
|
||||
{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ title }}
|
||||
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
|
||||
<a href="{{ form.vars.docu_chapter|docu_link }}" target="_blank"><i class="{{ 'help'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{% block form_body %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{% endblock %}
|
||||
{% block box_footer %}
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
|
||||
{% if back|default(false) %}
|
||||
<a href="{{ back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
|
||||
@@ -25,7 +25,9 @@
|
||||
{% if reset|default(true) %}
|
||||
<input type="reset" value="{{ 'action.reset'|trans }}" class="btn btn-link pull-right" />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block box_after %}
|
||||
{{ form_end(form) }}
|
||||
{% block form_after %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
<title>{% block title %}{{ model.template.title }}{% endblock %}</title>
|
||||
<title>{% block title %}{{ model.numberGenerator.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %}</title>
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
</head>
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
{% extends 'invoice/layout.html.twig' %}
|
||||
{% set language = model.template.language|default(app.request.locale) %}
|
||||
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||
|
||||
{% block invoice %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h2 class="page-header">
|
||||
<span contenteditable="true">{{ model.template.title }}</span>
|
||||
<small class="pull-right">{{ 'label.date'|trans }}: {{ model.invoiceDate|date_short }}</small>
|
||||
<small class="pull-right">{{ 'label.date'|trans({}, 'messages', language) }}: {{ model.invoiceDate|date_short }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
{{ 'invoice.from'|trans }}
|
||||
{{ 'invoice.from'|trans({}, 'messages', language) }}
|
||||
<address contenteditable="true">
|
||||
<strong>{{ model.template.company }}</strong><br>
|
||||
{{ model.template.address|trim|nl2br }}
|
||||
{% if model.template.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans }}:
|
||||
{{ 'label.vat_id'|trans({}, 'messages', language) }}:
|
||||
{{ model.template.vatId }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-5">
|
||||
{% set customerPhone = model.customer.phone|default(model.customer.mobile) %}
|
||||
{{ 'invoice.to'|trans }}
|
||||
{{ 'invoice.to'|trans({}, 'messages', language) }}
|
||||
<address contenteditable="true">
|
||||
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
|
||||
{{ model.customer.address|nl2br }}
|
||||
{% if model.customer.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
|
||||
{{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
|
||||
{% endif %}
|
||||
{% if model.customer.number is not empty %}
|
||||
<br>
|
||||
{{ 'label.number'|trans }}: {{ model.customer.number }}
|
||||
{{ 'label.number'|trans({}, 'messages', language) }}: {{ model.customer.number }}
|
||||
{% endif %}
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<br>
|
||||
{{ 'label.orderNumber'|trans }}: {{ model.query.project.orderNumber }}
|
||||
{{ 'label.orderNumber'|trans({}, 'messages', language) }}: {{ model.query.project.orderNumber }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</div>
|
||||
@@ -49,11 +50,11 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
<p contenteditable="true">
|
||||
<strong>{{ 'invoice.number'|trans }}:</strong>
|
||||
<strong>{{ 'invoice.number'|trans({}, 'messages', language) }}:</strong>
|
||||
{{ model.numberGenerator.invoiceNumber }}
|
||||
|
||||
<br>
|
||||
<strong>{{ 'invoice.due_days'|trans }}:</strong>
|
||||
<strong>{{ 'invoice.due_days'|trans({}, 'messages', language) }}:</strong>
|
||||
{{ model.dueDate|date_short }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -65,16 +66,16 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}</th>
|
||||
<th>{{ 'label.description'|trans }}</th>
|
||||
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
|
||||
<th class="text-right">{{ 'label.amount'|trans }}</th>
|
||||
<th class="text-right">{{ 'label.total_rate'|trans }}</th>
|
||||
<th>{{ 'label.date'|trans({}, 'messages', language) }}</th>
|
||||
<th>{{ 'label.description'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.unit_price'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.amount'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.total_rate'|trans({}, 'messages', language) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in model.calculator.entries %}
|
||||
{% set duration = entry.duration|duration_decimal %}
|
||||
{% set duration = entry.duration|duration(isDecimal) %}
|
||||
{% if entry.fixedRate %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
{% set duration = entry.amount|amount %}
|
||||
@@ -99,19 +100,19 @@
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
{{ 'invoice.subtotal'|trans }}
|
||||
{{ 'invoice.subtotal'|trans({}, 'messages', language) }}
|
||||
</td>
|
||||
<td class="text-right">{{ model.calculator.subtotal|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
|
||||
{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)
|
||||
</td>
|
||||
<td class="text-right">{{ model.calculator.tax|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right text-nowrap">
|
||||
<strong>{{ 'invoice.total'|trans }}</strong>
|
||||
<strong>{{ 'invoice.total'|trans({}, 'messages', language) }}</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<strong>{{ model.calculator.total|money(model.calculator.currency) }}</strong>
|
||||
@@ -134,11 +135,11 @@
|
||||
|
||||
<footer class="footer">
|
||||
<p>
|
||||
<strong>{{ 'label.address'|trans }}</strong>: {{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<strong>{{ 'label.address'|trans({}, 'messages', language) }}</strong>: {{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<br>
|
||||
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<strong>{{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<br>
|
||||
<strong>{{ 'label.contact'|trans }}</strong>: {{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<strong>{{ 'label.contact'|trans({}, 'messages', language) }}</strong>: {{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% extends 'invoice/layout.html.twig' %}
|
||||
{% set language = model.template.language|default(app.request.locale) %}
|
||||
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||
|
||||
{% block invoice %}
|
||||
|
||||
<div class="row" id="freelancer-invoice">
|
||||
<div class="col-xs-12">
|
||||
<header>
|
||||
@@ -12,33 +12,33 @@
|
||||
</address>
|
||||
</header>
|
||||
<article class="address">
|
||||
<h1>{{ 'invoice.to'|trans }}</h1>
|
||||
<h1>{{ 'invoice.to'|trans({}, 'messages', language) }}</h1>
|
||||
<address>
|
||||
<p>
|
||||
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
|
||||
{{ model.customer.address|nl2br }}
|
||||
{% if model.customer.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
|
||||
{{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</address>
|
||||
<table class="meta">
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}:</th>
|
||||
<th>{{ 'label.date'|trans({}, 'messages', language) }}:</th>
|
||||
<td contenteditable="true">{{ model.invoiceDate|date_short }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.service_date'|trans }}:</th>
|
||||
<td><span contenteditable="true">{{ model.query.end|month_name|trans }} {{ model.query.end|date('Y') }}</td>
|
||||
<th>{{ 'invoice.service_date'|trans({}, 'messages', language) }}:</th>
|
||||
<td><span contenteditable="true">{{ model.query.end|month_name|trans({}, 'messages', language) }} {{ model.query.end|date('Y') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.number'|trans }}:</th>
|
||||
<th>{{ 'invoice.number'|trans({}, 'messages', language) }}:</th>
|
||||
<td><span contenteditable="true">{{ model.numberGenerator.invoiceNumber }}</td>
|
||||
</tr>
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.orderNumber'|trans }}</th>
|
||||
<th>{{ 'label.orderNumber'|trans({}, 'messages', language) }}</th>
|
||||
<td contenteditable="true">
|
||||
{{ model.query.project.orderNumber }}
|
||||
</td>
|
||||
@@ -46,7 +46,7 @@
|
||||
{% endif %}
|
||||
{% if model.template.vatId is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.vat_id'|trans }}:</th>
|
||||
<th>{{ 'label.vat_id'|trans({}, 'messages', language) }}:</th>
|
||||
<td>
|
||||
{{ model.template.vatId }}
|
||||
</td>
|
||||
@@ -55,22 +55,19 @@
|
||||
</table>
|
||||
</article>
|
||||
<article class="invoice-items">
|
||||
<h2>{{ model.template.title|default('timesheet'|trans({}, 'invoice-renderer')) }}</h2>
|
||||
<p contenteditable="true">
|
||||
{{ 'label.invoice_salutation'|trans|nl2br }}
|
||||
</p>
|
||||
<h2>{{ model.template.title }}</h2>
|
||||
<table class="inventory">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.description'|trans }}</th>
|
||||
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
|
||||
<th class="text-right">{{ 'label.amount'|trans }}</th>
|
||||
<th class="text-right">{{ 'label.total_rate'|trans }}</th>
|
||||
<th>{{ 'label.description'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.unit_price'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.amount'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.total_rate'|trans({}, 'messages', language) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in model.calculator.entries %}
|
||||
{% set duration = entry.duration|duration %}
|
||||
{% set duration = entry.duration|duration(isDecimal) %}
|
||||
{% if entry.fixedRate is not null %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
{% set duration = entry.amount|amount %}
|
||||
@@ -95,15 +92,15 @@
|
||||
</table>
|
||||
<table class="balance">
|
||||
<tr>
|
||||
<th>{{ 'invoice.subtotal'|trans }}</th>
|
||||
<th>{{ 'invoice.subtotal'|trans({}, 'messages', language) }}</th>
|
||||
<td>{{ model.calculator.subtotal|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)</th>
|
||||
<th>{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)</th>
|
||||
<td>{{ model.calculator.tax|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="total">{{ 'invoice.total'|trans }}</th>
|
||||
<th class="total">{{ 'invoice.total'|trans({}, 'messages', language) }}</th>
|
||||
<td class="total">{{ model.calculator.total|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -119,7 +116,7 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-4" contenteditable="true">
|
||||
<p>
|
||||
<strong>{{ 'label.address'|trans }}</strong>
|
||||
<strong>{{ 'label.address'|trans({}, 'messages', language) }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{{ model.template.company }}<br>
|
||||
@@ -128,7 +125,7 @@
|
||||
</div>
|
||||
<div class="col-sm-4 text-center" contenteditable="true">
|
||||
<p>
|
||||
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>
|
||||
<strong>{{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{{ model.template.paymentDetails|nl2br }}
|
||||
@@ -136,7 +133,7 @@
|
||||
</div>
|
||||
<div class="col-sm-4 text-right" contenteditable="true">
|
||||
<p>
|
||||
<strong>{{ 'label.contact'|trans }}</strong>
|
||||
<strong>{{ 'label.contact'|trans({}, 'messages', language) }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{{ model.template.contact|nl2br }}
|
||||
@@ -146,5 +143,4 @@
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,11 +1,13 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% extends 'invoice/layout.html.twig' %}
|
||||
{% set language = model.template.language|default(app.request.locale) %}
|
||||
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||
|
||||
{% block invoice %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h2 class="page-header">
|
||||
<span contenteditable="true">{{ model.template.title|default('timesheet'|trans({}, 'invoice-renderer')) }}</span>
|
||||
<span contenteditable="true">{{ model.template.title }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +16,7 @@
|
||||
<div class="col-xs-12">
|
||||
<table class="table no-border table-condensed">
|
||||
<tr>
|
||||
<th>{{ 'invoice.from'|trans }}</th>
|
||||
<th>{{ 'invoice.from'|trans({}, 'messages', language) }}</th>
|
||||
<td contenteditable="true">
|
||||
{% if model.query.user is not empty %}
|
||||
{{ widgets.username(model.query.user) }}
|
||||
@@ -24,17 +26,17 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}</th>
|
||||
<th>{{ 'label.date'|trans({}, 'messages', language) }}</th>
|
||||
<td contenteditable="true">
|
||||
{% if model.query.begin|date('m') != model.query.end|date('m') or model.query.begin|date('Y') != model.query.end|date('Y') %}
|
||||
{{ model.query.begin|date_short }} - {{ model.query.end|date_short }}
|
||||
{% else %}
|
||||
{{ model.query.end|month_name|trans }} {{ model.query.end|date('Y') }}
|
||||
{{ model.query.end|month_name|trans({}, 'messages', language) }} {{ model.query.end|date('Y') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.customer'|trans }}</th>
|
||||
<th>{{ 'label.customer'|trans({}, 'messages', language) }}</th>
|
||||
<td contenteditable="true">
|
||||
{% if model.customer.number is not empty %}[{{ model.customer.number }}]{% endif %}
|
||||
{{ model.customer.name }}{% if model.customer.contact is not empty %} / {{ model.customer.contact }}{% endif %}
|
||||
@@ -42,7 +44,7 @@
|
||||
</tr>
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.orderNumber'|trans }}</th>
|
||||
<th>{{ 'label.orderNumber'|trans({}, 'messages', language) }}</th>
|
||||
<td contenteditable="true">
|
||||
{{ model.query.project.orderNumber }}
|
||||
</td>
|
||||
@@ -57,12 +59,12 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}</th>
|
||||
<th>{{ 'label.date'|trans({}, 'messages', language) }}</th>
|
||||
{% if model.query.user is empty %}
|
||||
<th>{{ 'label.user'|trans }}</th>
|
||||
<th>{{ 'label.user'|trans({}, 'messages', language) }}</th>
|
||||
{% endif %}
|
||||
<th>{{ 'label.activity'|trans }}</th>
|
||||
<th>{{ 'label.hours'|trans }}</th>
|
||||
<th>{{ 'label.activity'|trans({}, 'messages', language) }}</th>
|
||||
<th>{{ 'label.hours'|trans({}, 'messages', language) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -79,7 +81,7 @@
|
||||
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ entry.duration|duration }}</td>
|
||||
<td>{{ entry.duration|duration(isDecimal) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -89,8 +91,8 @@
|
||||
{% if model.query.user is empty %}
|
||||
<th></th>
|
||||
{% endif %}
|
||||
<th>{{ 'invoice.total_working_time'|trans }}</th>
|
||||
<th>{{ model.calculator.timeWorked|duration }}</th>
|
||||
<th>{{ 'invoice.total_working_time'|trans({}, 'messages', language) }}</th>
|
||||
<th>{{ model.calculator.timeWorked|duration(isDecimal) }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@@ -100,7 +102,7 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{% if model.template.paymentTerms is not empty %}
|
||||
<p class="lead">{{ 'label.payment_terms'|trans }}</p>
|
||||
<p class="lead">{{ 'label.payment_terms'|trans({}, 'messages', language) }}</p>
|
||||
|
||||
<p class="text-muted well well-sm no-shadow" contenteditable="true" style="margin-bottom: 100px">
|
||||
{{ model.template.paymentTerms|trim|nl2br }}
|
||||
@@ -111,10 +113,10 @@
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="padding-bottom: 60px">{{ 'invoice.signature_user'|trans }}</th>
|
||||
<th style="padding-bottom: 60px">{{ 'invoice.signature_user'|trans({}, 'messages', language) }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.signature_customer'|trans }}</th>
|
||||
<th>{{ 'invoice.signature_customer'|trans({}, 'messages', language) }}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -46,10 +46,22 @@
|
||||
{{ form_row(form.vat) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.renderer) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.language) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.calculator) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.numberGenerator) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
@@ -33,7 +33,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
$result = $client->getCrawler()->filter('section.content div.box.box-primary');
|
||||
$this->assertEquals(count($expectedForms), count($result));
|
||||
|
||||
$result = $client->getCrawler()->filter('section.content div.box.box-primary form');
|
||||
$result = $client->getCrawler()->filter('section.content form div.box.box-primary');
|
||||
$this->assertEquals(count($expectedForms), count($result));
|
||||
|
||||
foreach ($expectedForms as $formConfig) {
|
||||
|
||||
30
tests/Entity/InvoiceDocumentTest.php
Normal file
30
tests/Entity/InvoiceDocumentTest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\InvoiceDocument;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\InvoiceDocument
|
||||
*/
|
||||
class InvoiceDocumentTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$dir = realpath(__DIR__ . '/../../templates/invoice/renderer');
|
||||
$sut = new InvoiceDocument(new \SplFileInfo($dir . '/default.html.twig'));
|
||||
|
||||
self::assertEquals('twig', $sut->getFileExtension());
|
||||
self::assertStringContainsString('templates/invoice/renderer/default.html.twig', $sut->getFilename());
|
||||
self::assertEquals('default', $sut->getId());
|
||||
self::assertEquals('default.html.twig', $sut->getName());
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ class InvoiceTemplateTest extends TestCase
|
||||
self::assertEquals('default', $sut->getNumberGenerator());
|
||||
self::assertEquals('default', $sut->getRenderer());
|
||||
self::assertEquals(30, $sut->getDueDays());
|
||||
self::assertFalse($sut->isDecimalDuration());
|
||||
self::assertNull($sut->getLanguage());
|
||||
}
|
||||
|
||||
public function testSetNullForOptionalValues()
|
||||
@@ -46,6 +48,7 @@ class InvoiceTemplateTest extends TestCase
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setContact(null));
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setAddress(null));
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentTerms(null));
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setLanguage(null));
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
@@ -66,6 +69,12 @@ class InvoiceTemplateTest extends TestCase
|
||||
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setVatId('1234567890'));
|
||||
self::assertEquals('1234567890', $sut->getVatId());
|
||||
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setDecimalDuration(true));
|
||||
self::assertTrue($sut->isDecimalDuration());
|
||||
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setLanguage('de'));
|
||||
self::assertEquals('de', $sut->getLanguage());
|
||||
}
|
||||
|
||||
public function testToString()
|
||||
|
||||
93
tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php
Normal file
93
tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\Hydrator\InvoiceItemDefaultHydrator;
|
||||
use App\Tests\Invoice\Renderer\RendererTestTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\Hydrator\InvoiceItemDefaultHydrator
|
||||
*/
|
||||
class InvoiceItemDefaultHydratorTest extends TestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
public function testHydrate()
|
||||
{
|
||||
$model = $this->getInvoiceModel();
|
||||
|
||||
$sut = new InvoiceItemDefaultHydrator();
|
||||
$sut->setInvoiceModel($model);
|
||||
|
||||
$result = $sut->hydrate($model->getCalculator()->getEntries()[0]);
|
||||
$metaFields = ['entry.meta.foo-timesheet'];
|
||||
$this->assertEntryStructure($result, $metaFields);
|
||||
|
||||
$result = $sut->hydrate($model->getCalculator()->getEntries()[1]);
|
||||
$metaFields = ['entry.meta.foo-timesheet2'];
|
||||
$this->assertEntryStructure($result, $metaFields);
|
||||
}
|
||||
|
||||
protected function assertEntryStructure(array $model, array $metaFields)
|
||||
{
|
||||
$keys = [
|
||||
'entry.row',
|
||||
'entry.description',
|
||||
'entry.amount',
|
||||
'entry.rate',
|
||||
'entry.rate_nc',
|
||||
'entry.rate_plain',
|
||||
'entry.total',
|
||||
'entry.total_nc',
|
||||
'entry.total_plain',
|
||||
'entry.currency',
|
||||
'entry.duration',
|
||||
'entry.duration_decimal',
|
||||
'entry.duration_minutes',
|
||||
'entry.begin',
|
||||
'entry.begin_time',
|
||||
'entry.begin_timestamp',
|
||||
'entry.end',
|
||||
'entry.end_time',
|
||||
'entry.end_timestamp',
|
||||
'entry.date',
|
||||
'entry.user_id',
|
||||
'entry.user_name',
|
||||
'entry.user_alias',
|
||||
'entry.user_title',
|
||||
'entry.activity',
|
||||
'entry.activity_id',
|
||||
'entry.activity.meta.foo-activity',
|
||||
'entry.project',
|
||||
'entry.project_id',
|
||||
'entry.project.meta.foo-project',
|
||||
'entry.customer',
|
||||
'entry.customer_id',
|
||||
'entry.customer.meta.foo-customer',
|
||||
'entry.category',
|
||||
'entry.type',
|
||||
];
|
||||
|
||||
$keys = array_merge($keys, $metaFields);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$this->assertArrayHasKey($key, $model);
|
||||
}
|
||||
|
||||
$expectedKeys = array_merge([], $keys);
|
||||
sort($expectedKeys);
|
||||
$givenKeys = array_keys($model);
|
||||
sort($givenKeys);
|
||||
|
||||
$this->assertEquals($expectedKeys, $givenKeys);
|
||||
$this->assertEquals(count($keys), count($givenKeys));
|
||||
}
|
||||
}
|
||||
58
tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.php
Normal file
58
tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.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\Tests\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\Hydrator\InvoiceModelActivityHydrator;
|
||||
use App\Tests\Invoice\Renderer\RendererTestTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\Hydrator\InvoiceModelActivityHydrator
|
||||
*/
|
||||
class InvoiceModelActivityHydratorTest extends TestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
public function testHydrate()
|
||||
{
|
||||
$model = $this->getInvoiceModel();
|
||||
|
||||
$sut = new InvoiceModelActivityHydrator();
|
||||
|
||||
$result = $sut->hydrate($model);
|
||||
$this->assertModelStructure($result);
|
||||
|
||||
$model->getQuery()->setActivity(null);
|
||||
$result = $sut->hydrate($model);
|
||||
self::assertEmpty($result);
|
||||
}
|
||||
|
||||
protected function assertModelStructure(array $model)
|
||||
{
|
||||
$keys = [
|
||||
'activity.id',
|
||||
'activity.name',
|
||||
'activity.comment',
|
||||
'activity.fixed_rate',
|
||||
'activity.fixed_rate_nc',
|
||||
'activity.fixed_rate_plain',
|
||||
'activity.hourly_rate',
|
||||
'activity.hourly_rate_nc',
|
||||
'activity.hourly_rate_plain',
|
||||
'activity.meta.foo-activity',
|
||||
];
|
||||
|
||||
$givenKeys = array_keys($model);
|
||||
sort($keys);
|
||||
sort($givenKeys);
|
||||
|
||||
$this->assertEquals($keys, $givenKeys);
|
||||
}
|
||||
}
|
||||
65
tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php
Normal file
65
tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\Hydrator\InvoiceModelCustomerHydrator;
|
||||
use App\Tests\Invoice\Renderer\RendererTestTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\Hydrator\InvoiceModelCustomerHydrator
|
||||
*/
|
||||
class InvoiceModelCustomerHydratorTest extends TestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
public function testHydrate()
|
||||
{
|
||||
$model = $this->getInvoiceModel();
|
||||
|
||||
$sut = new InvoiceModelCustomerHydrator();
|
||||
|
||||
$result = $sut->hydrate($model);
|
||||
$this->assertModelStructure($result);
|
||||
|
||||
$model->setCustomer(null);
|
||||
$result = $sut->hydrate($model);
|
||||
self::assertEmpty($result);
|
||||
}
|
||||
|
||||
protected function assertModelStructure(array $model)
|
||||
{
|
||||
$keys = [
|
||||
'customer.id',
|
||||
'customer.address',
|
||||
'customer.name',
|
||||
'customer.contact',
|
||||
'customer.company',
|
||||
'customer.vat',
|
||||
'customer.country',
|
||||
'customer.number',
|
||||
'customer.homepage',
|
||||
'customer.comment',
|
||||
'customer.fixed_rate',
|
||||
'customer.fixed_rate_nc',
|
||||
'customer.fixed_rate_plain',
|
||||
'customer.hourly_rate',
|
||||
'customer.hourly_rate_nc',
|
||||
'customer.hourly_rate_plain',
|
||||
'customer.meta.foo-customer',
|
||||
];
|
||||
|
||||
$givenKeys = array_keys($model);
|
||||
sort($keys);
|
||||
sort($givenKeys);
|
||||
|
||||
$this->assertEquals($keys, $givenKeys);
|
||||
}
|
||||
}
|
||||
76
tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php
Normal file
76
tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\Hydrator\InvoiceModelDefaultHydrator;
|
||||
use App\Tests\Invoice\Renderer\RendererTestTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\Hydrator\InvoiceModelDefaultHydrator
|
||||
*/
|
||||
class InvoiceModelDefaultHydratorTest extends TestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
public function testHydrate()
|
||||
{
|
||||
$model = $this->getInvoiceModel();
|
||||
|
||||
$sut = new InvoiceModelDefaultHydrator();
|
||||
|
||||
$result = $sut->hydrate($model);
|
||||
$this->assertModelStructure($result);
|
||||
}
|
||||
|
||||
protected function assertModelStructure(array $model, $hasProject = true)
|
||||
{
|
||||
$keys = [
|
||||
'invoice.due_date',
|
||||
'invoice.date',
|
||||
'invoice.number',
|
||||
'invoice.currency',
|
||||
'invoice.currency_symbol',
|
||||
'invoice.vat',
|
||||
'invoice.tax',
|
||||
'invoice.tax_nc',
|
||||
'invoice.tax_plain',
|
||||
'invoice.total_time',
|
||||
'invoice.duration_decimal',
|
||||
'invoice.total',
|
||||
'invoice.total_nc',
|
||||
'invoice.total_plain',
|
||||
'invoice.subtotal',
|
||||
'invoice.subtotal_nc',
|
||||
'invoice.subtotal_plain',
|
||||
'template.name',
|
||||
'template.company',
|
||||
'template.address',
|
||||
'template.title',
|
||||
'template.payment_terms',
|
||||
'template.due_days',
|
||||
'template.vat_id',
|
||||
'template.contact',
|
||||
'template.payment_details',
|
||||
'query.begin',
|
||||
'query.day',
|
||||
'query.end',
|
||||
'query.month',
|
||||
'query.month_number',
|
||||
'query.year',
|
||||
];
|
||||
|
||||
$givenKeys = array_keys($model);
|
||||
sort($keys);
|
||||
sort($givenKeys);
|
||||
|
||||
$this->assertEquals($keys, $givenKeys);
|
||||
}
|
||||
}
|
||||
68
tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php
Normal file
68
tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\Hydrator\InvoiceModelProjectHydrator;
|
||||
use App\Tests\Invoice\Renderer\RendererTestTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\Hydrator\InvoiceModelProjectHydrator
|
||||
*/
|
||||
class InvoiceModelProjectHydratorTest extends TestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
public function testHydrate()
|
||||
{
|
||||
$model = $this->getInvoiceModel();
|
||||
|
||||
$sut = new InvoiceModelProjectHydrator();
|
||||
|
||||
$result = $sut->hydrate($model);
|
||||
$this->assertModelStructure($result);
|
||||
|
||||
$model->getQuery()->setProject(null);
|
||||
$result = $sut->hydrate($model);
|
||||
self::assertEmpty($result);
|
||||
}
|
||||
|
||||
protected function assertModelStructure(array $model)
|
||||
{
|
||||
$keys = [
|
||||
'project.id',
|
||||
'project.name',
|
||||
'project.comment',
|
||||
'project.order_date',
|
||||
'project.order_number',
|
||||
'project.fixed_rate',
|
||||
'project.fixed_rate_nc',
|
||||
'project.fixed_rate_plain',
|
||||
'project.hourly_rate',
|
||||
'project.hourly_rate_nc',
|
||||
'project.hourly_rate_plain',
|
||||
'project.meta.foo-project',
|
||||
'project.start_date',
|
||||
'project.end_date',
|
||||
'project.budget_money',
|
||||
'project.budget_money_nc',
|
||||
'project.budget_money_plain',
|
||||
'project.budget_time',
|
||||
'project.budget_time_decimal',
|
||||
'project.budget_time_minutes',
|
||||
];
|
||||
|
||||
$givenKeys = array_keys($model);
|
||||
sort($keys);
|
||||
sort($givenKeys);
|
||||
|
||||
$this->assertEquals($keys, $givenKeys);
|
||||
}
|
||||
}
|
||||
50
tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php
Normal file
50
tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\Hydrator\InvoiceModelUserHydrator;
|
||||
use App\Tests\Invoice\Renderer\RendererTestTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\Hydrator\InvoiceModelUserHydrator
|
||||
*/
|
||||
class InvoiceModelUserHydratorTest extends TestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
public function testHydrate()
|
||||
{
|
||||
$model = $this->getInvoiceModel();
|
||||
|
||||
$sut = new InvoiceModelUserHydrator();
|
||||
|
||||
$result = $sut->hydrate($model);
|
||||
$this->assertModelStructure($result);
|
||||
}
|
||||
|
||||
protected function assertModelStructure(array $model)
|
||||
{
|
||||
$keys = [
|
||||
'user.alias',
|
||||
'user.email',
|
||||
'user.name',
|
||||
'user.title',
|
||||
'user.meta.hello',
|
||||
'user.meta.kitty',
|
||||
];
|
||||
|
||||
$givenKeys = array_keys($model);
|
||||
sort($keys);
|
||||
sort($givenKeys);
|
||||
|
||||
$this->assertEquals($keys, $givenKeys);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class CsvRendererTest extends TestCase
|
||||
|
||||
$file = $response->getFile();
|
||||
$this->assertEquals('text/csv', $response->headers->get('Content-Type'));
|
||||
$this->assertEquals('attachment; filename=export.csv', $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals('attachment; filename=200204-customer_with_special_name.csv', $response->headers->get('Content-Disposition'));
|
||||
|
||||
$this->assertTrue(file_exists($file->getRealPath()));
|
||||
$content = file_get_contents($file->getRealPath());
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
namespace App\Tests\Invoice\Renderer;
|
||||
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceItem;
|
||||
use App\Invoice\InvoiceItemHydrator;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceModelHydrator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
@@ -29,6 +32,26 @@ class DebugRendererTest extends TestCase
|
||||
*/
|
||||
public function testRender(InvoiceModel $model, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3, $hasProject, $metaFields = [])
|
||||
{
|
||||
$itemHydrator = new class() implements InvoiceItemHydrator {
|
||||
public function setInvoiceModel(InvoiceModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
public function hydrate(InvoiceItem $item): array
|
||||
{
|
||||
return ['testFromItemHydrator' => 'foo'];
|
||||
}
|
||||
};
|
||||
$model->addItemHydrator($itemHydrator);
|
||||
|
||||
$modelHydrator = new class() implements InvoiceModelHydrator {
|
||||
public function hydrate(InvoiceModel $model): array
|
||||
{
|
||||
return ['testFromModelHydrator' => 'foo'];
|
||||
}
|
||||
};
|
||||
$model->addModelHydrator($modelHydrator);
|
||||
|
||||
$document = new InvoiceDocument(new \SplFileInfo(__DIR__ . '/DebugRenderer.php'));
|
||||
$sut = new DebugRenderer();
|
||||
/** @var Response $response */
|
||||
@@ -119,6 +142,7 @@ class DebugRendererTest extends TestCase
|
||||
'user.title',
|
||||
'user.meta.hello',
|
||||
'user.meta.kitty',
|
||||
'testFromModelHydrator'
|
||||
];
|
||||
|
||||
if ($hasProject) {
|
||||
@@ -191,6 +215,7 @@ class DebugRendererTest extends TestCase
|
||||
'entry.customer.meta.foo-customer',
|
||||
'entry.category',
|
||||
'entry.type',
|
||||
'testFromItemHydrator'
|
||||
];
|
||||
|
||||
$keys = array_merge($keys, $metaFields);
|
||||
|
||||
@@ -47,7 +47,7 @@ class DocxRendererTest extends TestCase
|
||||
|
||||
$file = $response->getFile();
|
||||
$this->assertEquals('application/vnd.openxmlformats-officedocument.wordprocessingml.document', $response->headers->get('Content-Type'));
|
||||
$this->assertEquals('attachment; filename=company.docx', $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals('attachment; filename=200204-customer_with_special_name.docx', $response->headers->get('Content-Disposition'));
|
||||
|
||||
$this->assertTrue(file_exists($file->getRealPath()));
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class OdsRendererTest extends TestCase
|
||||
|
||||
$file = $response->getFile();
|
||||
$this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
|
||||
$this->assertEquals('attachment; filename=open-spreadsheet.ods', $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals('attachment; filename=200204-customer_with_special_name.ods', $response->headers->get('Content-Disposition'));
|
||||
|
||||
$this->assertTrue(file_exists($file->getRealPath()));
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ trait RendererTestTrait
|
||||
$user->addPreference((new UserPreference())->setName('hello')->setValue('world'));
|
||||
|
||||
$customer = new Customer();
|
||||
$customer->setName('customer,with/special#name');
|
||||
$customer->setCurrency('EUR');
|
||||
$customer->setMetaField((new CustomerMeta())->setName('foo-customer')->setValue('bar-customer')->setIsVisible(true));
|
||||
|
||||
@@ -232,6 +233,7 @@ trait RendererTestTrait
|
||||
$user->addPreference((new UserPreference())->setName('hello')->setValue('world'));
|
||||
|
||||
$customer = new Customer();
|
||||
$customer->setName('customer,with/special#name');
|
||||
$customer->setCurrency('USD');
|
||||
$customer->setMetaField((new CustomerMeta())->setName('foo-customer')->setValue('bar-customer')->setIsVisible(true));
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ class TwigRendererTest extends KernelTestCase
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
$this->assertStringContainsString('<title>200204-customer_with_special_name</title>', $content);
|
||||
$this->assertStringContainsString('<h2 class="page-header">
|
||||
<span contenteditable="true">a very *long* test invoice / template title with [special] character</span>
|
||||
</h2>', $content);
|
||||
|
||||
@@ -58,7 +58,7 @@ class XlsxRendererTest extends TestCase
|
||||
|
||||
$file = $response->getFile();
|
||||
$this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
|
||||
$this->assertEquals('attachment; filename=spreadsheet.xlsx', $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals('attachment; filename=200204-customer_with_special_name.xlsx', $response->headers->get('Content-Disposition'));
|
||||
|
||||
$this->assertTrue(file_exists($file->getRealPath()));
|
||||
|
||||
|
||||
@@ -216,11 +216,11 @@ class ExtensionsTest extends TestCase
|
||||
|
||||
$sut = $this->getSut($this->localeEn);
|
||||
$this->assertEquals('02:37 h', $sut->duration($record->getDuration()));
|
||||
$this->assertEquals('02:37:17 h', $sut->duration($record->getDuration(), '%h:%m:%s h'));
|
||||
$this->assertEquals('2.62', $sut->duration($record->getDuration(), true));
|
||||
|
||||
// test Timesheet object
|
||||
$this->assertEquals('02:37 h', $sut->duration($record));
|
||||
$this->assertEquals('02:37:17', $sut->duration($record, '%h:%m:%s'));
|
||||
$this->assertEquals('2.62', $sut->duration($record, true));
|
||||
|
||||
// test extended format
|
||||
$sut = $this->getSut($this->localeFake, 'XX');
|
||||
@@ -237,6 +237,7 @@ class ExtensionsTest extends TestCase
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
|
||||
$this->assertEquals('00:00 h', $sut->duration(null));
|
||||
$this->assertEquals('0', $sut->duration(null, true));
|
||||
}
|
||||
|
||||
public function testDurationDecimal()
|
||||
|
||||
@@ -653,14 +653,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>حساب البنك</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
السيدات والسادة،
|
||||
|
||||
أشكرك على طلبك ورسوم خدماتي:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Month names
|
||||
|
||||
@@ -864,14 +864,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bankovní účet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Dámi a pánové,
|
||||
|
||||
Děkuji za vaši objednávku a využití mých složeb:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -908,14 +908,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bankkonto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Damer og herrer,
|
||||
|
||||
Jeg takker jer for jeres ordre og anmoder for min ydelse:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -928,13 +928,9 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bankverbindung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Sehr geehrte Damen und Herren,
|
||||
|
||||
für Ihren Auftrag bedanke ich mich und berechne für meine Leistungen:
|
||||
</target>
|
||||
<trans-unit id="label.decimalDuration">
|
||||
<source>label.decimalDuration</source>
|
||||
<target>Dauer als Dezimalzahl anzeigen</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -928,13 +928,9 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bank account</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Ladies and gentlemen,
|
||||
|
||||
I thank you for your order and charge for my services:
|
||||
</target>
|
||||
<trans-unit id="label.decimalDuration">
|
||||
<source>label.decimalDuration</source>
|
||||
<target>Display duration as decimal number</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -876,14 +876,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Cuenta bancaria</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Señoras y señores,
|
||||
|
||||
Le agradezco su pedido y cobro por mis servicios:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -916,14 +916,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Kontu Korronte Zenbakia</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Jaun-Andreok,
|
||||
|
||||
Zuen eskaria eskertzen dut eta ondorengoa ordaindu beharko duzue:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -804,15 +804,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>N° de compte bancaire</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Madame, Monsieur,
|
||||
|
||||
Je vous remercie pour votre commande.
|
||||
Vous trouverez ci-après la facture pour mes services :
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -804,14 +804,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bankszámlaszám</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Tisztelt Hölgyem/Uram!
|
||||
|
||||
Köszönöm megbízását, alább találja a szolgáltatásaim árát részletezve:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -804,12 +804,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>銀行口座</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
ご注文を頂きましたことならびにお支払い頂きますことに対して深く感謝申し上げます:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -824,14 +824,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target state="translated">계좌 번호</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target state="translated">
|
||||
고객님께,
|
||||
|
||||
저희 사무실에 업무를 맡겨 주셔서 감사드립니다:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -876,14 +876,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bankrekening</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Dames en heren,
|
||||
|
||||
Ik dank u voor uw bestelling en reken voor mijn diensten aan:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -804,14 +804,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Conta Bancária</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Senhoras e senhores,
|
||||
|
||||
Agradeço pelo seu pedido e cobro pelos meus serviços:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -953,14 +953,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bankový účet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Dámy a páni,
|
||||
|
||||
Ďakujem vám za vašu objednávku a poplatok za moje služby:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -799,12 +799,6 @@ För närvarande har %user% användare %records% tidsrekord som räknas upp till
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target state="translated">Bankkonto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target state="translated">Damer och herrar,
|
||||
|
||||
Jag tackar för din beställning och tar betalt för mina tjänster:</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -876,14 +876,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target state="translated">Banka hesabı</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target state="translated">
|
||||
Beyler ve bayanlar,
|
||||
|
||||
Siparişiniz ve ödemeleriniz için teşekkür ederim:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
@@ -907,14 +907,6 @@
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>银行账户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
女士们、先生们,
|
||||
|
||||
感谢您的订购并为我的服务收费:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Export
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user