diff --git a/composer.json b/composer.json
index 1e264ab3..b425fe4d 100644
--- a/composer.json
+++ b/composer.json
@@ -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"
diff --git a/composer.lock b/composer.lock
index 2544209a..e26339b8 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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"
}
}
diff --git a/config/bundles.php b/config/bundles.php
index 2c277c88..42cae898 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -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],
];
diff --git a/config/packages/twig_extensions.yaml b/config/packages/twig_extensions.yaml
deleted file mode 100644
index a61e7553..00000000
--- a/config/packages/twig_extensions.yaml
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/src/Entity/InvoiceDocument.php b/src/Entity/InvoiceDocument.php
index af131272..26274b65 100644
--- a/src/Entity/InvoiceDocument.php
+++ b/src/Entity/InvoiceDocument.php
@@ -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();
+ }
}
diff --git a/src/Entity/InvoiceTemplate.php b/src/Entity/InvoiceTemplate.php
index 734c10bb..da430bf9 100644
--- a/src/Entity/InvoiceTemplate.php
+++ b/src/Entity/InvoiceTemplate.php
@@ -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
*/
diff --git a/src/Form/InvoiceTemplateForm.php b/src/Form/InvoiceTemplateForm.php
index 336c87b2..0d001f10 100644
--- a/src/Form/InvoiceTemplateForm.php
+++ b/src/Form/InvoiceTemplateForm.php
@@ -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'
+ ])
;
}
diff --git a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
new file mode 100644
index 00000000..8cebdfbc
--- /dev/null
+++ b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
@@ -0,0 +1,135 @@
+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;
+ }
+}
diff --git a/src/Invoice/Hydrator/InvoiceModelActivityHydrator.php b/src/Invoice/Hydrator/InvoiceModelActivityHydrator.php
new file mode 100644
index 00000000..8160b4e2
--- /dev/null
+++ b/src/Invoice/Hydrator/InvoiceModelActivityHydrator.php
@@ -0,0 +1,48 @@
+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;
+ }
+}
diff --git a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php
new file mode 100644
index 00000000..7014eebf
--- /dev/null
+++ b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php
@@ -0,0 +1,55 @@
+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;
+ }
+}
diff --git a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php
new file mode 100644
index 00000000..ffb44b3e
--- /dev/null
+++ b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php
@@ -0,0 +1,64 @@
+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;
+ }
+}
diff --git a/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php b/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php
new file mode 100644
index 00000000..a8ac5de8
--- /dev/null
+++ b/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php
@@ -0,0 +1,58 @@
+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;
+ }
+}
diff --git a/src/Invoice/Hydrator/InvoiceModelUserHydrator.php b/src/Invoice/Hydrator/InvoiceModelUserHydrator.php
new file mode 100644
index 00000000..8fea17e6
--- /dev/null
+++ b/src/Invoice/Hydrator/InvoiceModelUserHydrator.php
@@ -0,0 +1,42 @@
+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;
+ }
+}
diff --git a/src/Invoice/InvoiceItemHydrator.php b/src/Invoice/InvoiceItemHydrator.php
new file mode 100644
index 00000000..714c330c
--- /dev/null
+++ b/src/Invoice/InvoiceItemHydrator.php
@@ -0,0 +1,17 @@
+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;
diff --git a/src/Invoice/InvoiceModelHydrator.php b/src/Invoice/InvoiceModelHydrator.php
new file mode 100644
index 00000000..8d5b481b
--- /dev/null
+++ b/src/Invoice/InvoiceModelHydrator.php
@@ -0,0 +1,15 @@
+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
diff --git a/src/Invoice/Renderer/AbstractSpreadsheetRenderer.php b/src/Invoice/Renderer/AbstractSpreadsheetRenderer.php
index f9a1570b..25547c66 100644
--- a/src/Invoice/Renderer/AbstractSpreadsheetRenderer.php
+++ b/src/Invoice/Renderer/AbstractSpreadsheetRenderer.php
@@ -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);
}
/**
diff --git a/src/Invoice/Renderer/DocxRenderer.php b/src/Invoice/Renderer/DocxRenderer.php
index 07779d6f..c88ad540 100644
--- a/src/Invoice/Renderer/DocxRenderer.php
+++ b/src/Invoice/Renderer/DocxRenderer.php
@@ -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);
}
diff --git a/src/Migrations/Version20200204124425.php b/src/Migrations/Version20200204124425.php
new file mode 100644
index 00000000..0aaa2215
--- /dev/null
+++ b/src/Migrations/Version20200204124425.php
@@ -0,0 +1,52 @@
+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');
+ }
+}
diff --git a/src/Twig/Extensions.php b/src/Twig/Extensions.php
index d29881fe..07d39fa4 100644
--- a/src/Twig/Extensions.php
+++ b/src/Twig/Extensions.php
@@ -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);
}
diff --git a/symfony.lock b/symfony.lock
index e53b54fa..b420912e 100644
--- a/symfony.lock
+++ b/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"
diff --git a/templates/base.html.twig b/templates/base.html.twig
index 9daa44c3..3dffd13d 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -58,7 +58,7 @@
{% endblock %}
{% block footer %}
-
+
{% endblock %}
{% block navbar_start %}
diff --git a/templates/default/_form.html.twig b/templates/default/_form.html.twig
index b9417f75..5e5971e5 100644
--- a/templates/default/_form.html.twig
+++ b/templates/default/_form.html.twig
@@ -1,31 +1,33 @@
{% if form_theme is defined %}
{% form_theme form form_theme %}
{% endif %}
-
- {% block form_before %}{% endblock %}
- {{ form_start(form) }}
-
-
+{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'boxtype': 'primary'} %}
+ {% block box_before %}
+ {% block form_before %}{% endblock %}
+ {{ form_start(form) }}
+ {% endblock %}
+ {% block box_title %}
+ {{ title }}
+ {% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
+
+ {% endif %}
+ {% endblock %}
+ {% block box_body %}
{% block form_body %}
{{ form_widget(form) }}
{% endblock %}
-
-
- {{ form_end(form) }}
- {% block form_after %}{% endblock %}
-
\ No newline at end of file
+ {% endblock %}
+ {% block box_after %}
+ {{ form_end(form) }}
+ {% block form_after %}{% endblock %}
+ {% endblock %}
+{% endembed %}
\ No newline at end of file
diff --git a/templates/invoice/layout.html.twig b/templates/invoice/layout.html.twig
index d0a55996..fcd91c84 100644
--- a/templates/invoice/layout.html.twig
+++ b/templates/invoice/layout.html.twig
@@ -4,7 +4,7 @@
- {% block title %}{{ model.template.title }}{% endblock %}
+ {% block title %}{{ model.numberGenerator.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %}
{{ encore_entry_link_tags('app') }}
{{ encore_entry_script_tags('app') }}
diff --git a/templates/invoice/renderer/default.html.twig b/templates/invoice/renderer/default.html.twig
index b0e7c49b..5bf82acd 100644
--- a/templates/invoice/renderer/default.html.twig
+++ b/templates/invoice/renderer/default.html.twig
@@ -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 %}
- {{ 'invoice.from'|trans }}
+ {{ 'invoice.from'|trans({}, 'messages', language) }}
{{ model.template.company }}
{{ model.template.address|trim|nl2br }}
{% if model.template.vatId is not empty %}
- {{ 'label.vat_id'|trans }}:
+ {{ 'label.vat_id'|trans({}, 'messages', language) }}:
{{ model.template.vatId }}
{% endif %}
- {% set customerPhone = model.customer.phone|default(model.customer.mobile) %}
- {{ 'invoice.to'|trans }}
+ {{ 'invoice.to'|trans({}, 'messages', language) }}
{{ model.customer.company|default(model.customer.name) }}
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
- {{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
+ {{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
{% endif %}
{% if model.customer.number is not empty %}
- {{ '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 %}
- {{ 'label.orderNumber'|trans }}: {{ model.query.project.orderNumber }}
+ {{ 'label.orderNumber'|trans({}, 'messages', language) }}: {{ model.query.project.orderNumber }}
{% endif %}
@@ -49,11 +50,11 @@
- {{ 'invoice.number'|trans }}:
+ {{ 'invoice.number'|trans({}, 'messages', language) }}:
{{ model.numberGenerator.invoiceNumber }}
- {{ 'invoice.due_days'|trans }}:
+ {{ 'invoice.due_days'|trans({}, 'messages', language) }}:
{{ model.dueDate|date_short }}
@@ -65,16 +66,16 @@
- {{ 'label.date'|trans }}
- {{ 'label.description'|trans }}
- {{ 'label.unit_price'|trans }}
- {{ 'label.amount'|trans }}
- {{ 'label.total_rate'|trans }}
+ {{ 'label.date'|trans({}, 'messages', language) }}
+ {{ 'label.description'|trans({}, 'messages', language) }}
+ {{ 'label.unit_price'|trans({}, 'messages', language) }}
+ {{ 'label.amount'|trans({}, 'messages', language) }}
+ {{ 'label.total_rate'|trans({}, 'messages', language) }}
{% 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 @@
- {{ 'invoice.subtotal'|trans }}
+ {{ 'invoice.subtotal'|trans({}, 'messages', language) }}
{{ model.calculator.subtotal|money(model.calculator.currency) }}
- {{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
+ {{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)
{{ model.calculator.tax|money(model.calculator.currency) }}
- {{ 'invoice.total'|trans }}
+ {{ 'invoice.total'|trans({}, 'messages', language) }}
{{ model.calculator.total|money(model.calculator.currency) }}
@@ -134,11 +135,11 @@
diff --git a/templates/invoice/renderer/freelancer.html.twig b/templates/invoice/renderer/freelancer.html.twig
index 743c6f74..2f7a0ba1 100644
--- a/templates/invoice/renderer/freelancer.html.twig
+++ b/templates/invoice/renderer/freelancer.html.twig
@@ -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 %}
-
- {{ 'invoice.to'|trans }}
+ {{ 'invoice.to'|trans({}, 'messages', language) }}
{{ model.customer.company|default(model.customer.name) }}
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
- {{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
+ {{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
{% endif %}
- {{ model.template.title|default('timesheet'|trans({}, 'invoice-renderer')) }}
-
- {{ 'label.invoice_salutation'|trans|nl2br }}
-
+ {{ model.template.title }}
- {{ 'label.description'|trans }}
- {{ 'label.unit_price'|trans }}
- {{ 'label.amount'|trans }}
- {{ 'label.total_rate'|trans }}
+ {{ 'label.description'|trans({}, 'messages', language) }}
+ {{ 'label.unit_price'|trans({}, 'messages', language) }}
+ {{ 'label.amount'|trans({}, 'messages', language) }}
+ {{ 'label.total_rate'|trans({}, 'messages', language) }}
{% 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 @@
- {{ 'invoice.subtotal'|trans }}
+ {{ 'invoice.subtotal'|trans({}, 'messages', language) }}
{{ model.calculator.subtotal|money(model.calculator.currency) }}
- {{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
+ {{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)
{{ model.calculator.tax|money(model.calculator.currency) }}
- {{ 'invoice.total'|trans }}
+ {{ 'invoice.total'|trans({}, 'messages', language) }}
{{ model.calculator.total|money(model.calculator.currency) }}
@@ -119,7 +116,7 @@
- {{ 'label.address'|trans }}
+ {{ 'label.address'|trans({}, 'messages', language) }}
{{ model.template.company }}
@@ -128,7 +125,7 @@
- {{ 'label.invoice_bank_account'|trans }}
+ {{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}
{{ model.template.paymentDetails|nl2br }}
@@ -136,7 +133,7 @@
- {{ 'label.contact'|trans }}
+ {{ 'label.contact'|trans({}, 'messages', language) }}
{{ model.template.contact|nl2br }}
@@ -146,5 +143,4 @@
-
{% endblock %}
\ No newline at end of file
diff --git a/templates/invoice/renderer/timesheet.html.twig b/templates/invoice/renderer/timesheet.html.twig
index 2c9359e1..411cba21 100644
--- a/templates/invoice/renderer/timesheet.html.twig
+++ b/templates/invoice/renderer/timesheet.html.twig
@@ -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 %}
@@ -14,7 +16,7 @@
- {{ 'invoice.from'|trans }}
+ {{ 'invoice.from'|trans({}, 'messages', language) }}
{% if model.query.user is not empty %}
{{ widgets.username(model.query.user) }}
@@ -24,17 +26,17 @@
- {{ 'label.date'|trans }}
+ {{ 'label.date'|trans({}, 'messages', language) }}
{% 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 %}
- {{ 'label.customer'|trans }}
+ {{ 'label.customer'|trans({}, 'messages', language) }}
{% 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 @@
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
- {{ 'label.orderNumber'|trans }}
+ {{ 'label.orderNumber'|trans({}, 'messages', language) }}
{{ model.query.project.orderNumber }}
@@ -57,12 +59,12 @@
- {{ 'label.date'|trans }}
+ {{ 'label.date'|trans({}, 'messages', language) }}
{% if model.query.user is empty %}
- {{ 'label.user'|trans }}
+ {{ 'label.user'|trans({}, 'messages', language) }}
{% endif %}
- {{ 'label.activity'|trans }}
- {{ 'label.hours'|trans }}
+ {{ 'label.activity'|trans({}, 'messages', language) }}
+ {{ 'label.hours'|trans({}, 'messages', language) }}
@@ -79,7 +81,7 @@
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
{% endif %}
- {{ entry.duration|duration }}
+ {{ entry.duration|duration(isDecimal) }}
{% endfor %}
@@ -89,8 +91,8 @@
{% if model.query.user is empty %}
{% endif %}
- {{ 'invoice.total_working_time'|trans }}
- {{ model.calculator.timeWorked|duration }}
+ {{ 'invoice.total_working_time'|trans({}, 'messages', language) }}
+ {{ model.calculator.timeWorked|duration(isDecimal) }}
@@ -100,7 +102,7 @@
{% if model.template.paymentTerms is not empty %}
-
{{ 'label.payment_terms'|trans }}
+
{{ 'label.payment_terms'|trans({}, 'messages', language) }}
{{ model.template.paymentTerms|trim|nl2br }}
@@ -111,10 +113,10 @@
- {{ 'invoice.signature_user'|trans }}
+ {{ 'invoice.signature_user'|trans({}, 'messages', language) }}
- {{ 'invoice.signature_customer'|trans }}
+ {{ 'invoice.signature_customer'|trans({}, 'messages', language) }}
diff --git a/templates/invoice/template_edit.html.twig b/templates/invoice/template_edit.html.twig
index 0096ea98..b075b17e 100644
--- a/templates/invoice/template_edit.html.twig
+++ b/templates/invoice/template_edit.html.twig
@@ -46,10 +46,22 @@
{{ form_row(form.vat) }}
-
- {{ form_row(form.renderer) }}
- {{ form_row(form.calculator) }}
- {{ form_row(form.numberGenerator) }}
+
+
+ {{ form_row(form.renderer) }}
+
+
+ {{ form_row(form.language) }}
+
+
+
+
+ {{ form_row(form.calculator) }}
+
+
+ {{ form_row(form.numberGenerator) }}
+
+
{{ form_widget(form) }}
{% endblock %}
{% endembed %}
diff --git a/tests/Controller/SystemConfigurationControllerTest.php b/tests/Controller/SystemConfigurationControllerTest.php
index 2d8aa328..3eac2f7b 100644
--- a/tests/Controller/SystemConfigurationControllerTest.php
+++ b/tests/Controller/SystemConfigurationControllerTest.php
@@ -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) {
diff --git a/tests/Entity/InvoiceDocumentTest.php b/tests/Entity/InvoiceDocumentTest.php
new file mode 100644
index 00000000..4a5f3fb4
--- /dev/null
+++ b/tests/Entity/InvoiceDocumentTest.php
@@ -0,0 +1,30 @@
+getFileExtension());
+ self::assertStringContainsString('templates/invoice/renderer/default.html.twig', $sut->getFilename());
+ self::assertEquals('default', $sut->getId());
+ self::assertEquals('default.html.twig', $sut->getName());
+ }
+}
diff --git a/tests/Entity/InvoiceTemplateTest.php b/tests/Entity/InvoiceTemplateTest.php
index 2297bf36..8c9017c8 100644
--- a/tests/Entity/InvoiceTemplateTest.php
+++ b/tests/Entity/InvoiceTemplateTest.php
@@ -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()
diff --git a/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php b/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php
new file mode 100644
index 00000000..e7030ffb
--- /dev/null
+++ b/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php
@@ -0,0 +1,93 @@
+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));
+ }
+}
diff --git a/tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.php
new file mode 100644
index 00000000..fe1bcebf
--- /dev/null
+++ b/tests/Invoice/Hydrator/InvoiceModelActivityHydratorTest.php
@@ -0,0 +1,58 @@
+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);
+ }
+}
diff --git a/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php
new file mode 100644
index 00000000..4f9a2e83
--- /dev/null
+++ b/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php
@@ -0,0 +1,65 @@
+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);
+ }
+}
diff --git a/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php
new file mode 100644
index 00000000..1a179f2f
--- /dev/null
+++ b/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php
@@ -0,0 +1,76 @@
+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);
+ }
+}
diff --git a/tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php
new file mode 100644
index 00000000..b5b11491
--- /dev/null
+++ b/tests/Invoice/Hydrator/InvoiceModelProjectHydratorTest.php
@@ -0,0 +1,68 @@
+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);
+ }
+}
diff --git a/tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php
new file mode 100644
index 00000000..c6386f19
--- /dev/null
+++ b/tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php
@@ -0,0 +1,50 @@
+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);
+ }
+}
diff --git a/tests/Invoice/Renderer/CsvRendererTest.php b/tests/Invoice/Renderer/CsvRendererTest.php
index c510cecc..28831295 100644
--- a/tests/Invoice/Renderer/CsvRendererTest.php
+++ b/tests/Invoice/Renderer/CsvRendererTest.php
@@ -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());
diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php
index 075304f4..207a8056 100644
--- a/tests/Invoice/Renderer/DebugRendererTest.php
+++ b/tests/Invoice/Renderer/DebugRendererTest.php
@@ -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);
diff --git a/tests/Invoice/Renderer/DocxRendererTest.php b/tests/Invoice/Renderer/DocxRendererTest.php
index 8b0423a0..22d462c4 100644
--- a/tests/Invoice/Renderer/DocxRendererTest.php
+++ b/tests/Invoice/Renderer/DocxRendererTest.php
@@ -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()));
diff --git a/tests/Invoice/Renderer/OdsRendererTest.php b/tests/Invoice/Renderer/OdsRendererTest.php
index 753879a8..9b6c8362 100644
--- a/tests/Invoice/Renderer/OdsRendererTest.php
+++ b/tests/Invoice/Renderer/OdsRendererTest.php
@@ -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()));
diff --git a/tests/Invoice/Renderer/RendererTestTrait.php b/tests/Invoice/Renderer/RendererTestTrait.php
index 3b4b5eb6..7548f220 100644
--- a/tests/Invoice/Renderer/RendererTestTrait.php
+++ b/tests/Invoice/Renderer/RendererTestTrait.php
@@ -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));
diff --git a/tests/Invoice/Renderer/TwigRendererTest.php b/tests/Invoice/Renderer/TwigRendererTest.php
index 7b621845..d4a8e16d 100644
--- a/tests/Invoice/Renderer/TwigRendererTest.php
+++ b/tests/Invoice/Renderer/TwigRendererTest.php
@@ -62,6 +62,7 @@ class TwigRendererTest extends KernelTestCase
$content = $response->getContent();
+ $this->assertStringContainsString('200204-customer_with_special_name ', $content);
$this->assertStringContainsString('', $content);
diff --git a/tests/Invoice/Renderer/XlsxRendererTest.php b/tests/Invoice/Renderer/XlsxRendererTest.php
index c7a1ae53..0bb57b95 100644
--- a/tests/Invoice/Renderer/XlsxRendererTest.php
+++ b/tests/Invoice/Renderer/XlsxRendererTest.php
@@ -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()));
diff --git a/tests/Twig/ExtensionsTest.php b/tests/Twig/ExtensionsTest.php
index a4408d70..96dffe69 100644
--- a/tests/Twig/ExtensionsTest.php
+++ b/tests/Twig/ExtensionsTest.php
@@ -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()
diff --git a/translations/messages.ar.xlf b/translations/messages.ar.xlf
index 94d8d1f6..e5c23646 100755
--- a/translations/messages.ar.xlf
+++ b/translations/messages.ar.xlf
@@ -653,14 +653,6 @@
label.invoice_bank_account
حساب البنك
-
- label.invoice_salutation
-
- السيدات والسادة،
-
- أشكرك على طلبك ورسوم خدماتي:
-
-
diff --git a/translations/messages.hu.xlf b/translations/messages.hu.xlf
index b2a73c68..f8839d96 100644
--- a/translations/messages.hu.xlf
+++ b/translations/messages.hu.xlf
@@ -804,14 +804,6 @@
label.invoice_bank_account
Bankszámlaszám
-
- label.invoice_salutation
-
- Tisztelt Hölgyem/Uram!
-
- Köszönöm megbízását, alább találja a szolgáltatásaim árát részletezve:
-
-