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) }} -
-

- {{ title }} - {% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %} - - {% endif %} -

-
-
+{% 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 @@ - - - - - + + + + + {% 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 @@
{{ '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) }}
- {{ '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 @@

- {{ 'label.address'|trans }}: {{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }} + {{ 'label.address'|trans({}, 'messages', language) }}: {{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
- {{ 'label.invoice_bank_account'|trans }}: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }} + {{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
- {{ 'label.contact'|trans }}: {{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }} + {{ 'label.contact'|trans({}, 'messages', language) }}: {{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}

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 %} -
@@ -12,33 +12,33 @@
-

{{ '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.query.project is not empty and model.query.project.orderNumber is not empty %} - + @@ -46,7 +46,7 @@ {% endif %} {% if model.template.vatId is not empty %} - + @@ -55,22 +55,19 @@
{{ 'label.date'|trans }}:{{ 'label.date'|trans({}, 'messages', language) }}: {{ model.invoiceDate|date_short }}
{{ 'invoice.service_date'|trans }}:{{ model.query.end|month_name|trans }} {{ model.query.end|date('Y') }}{{ 'invoice.service_date'|trans({}, 'messages', language) }}:{{ model.query.end|month_name|trans({}, 'messages', language) }} {{ model.query.end|date('Y') }}
{{ 'invoice.number'|trans }}:{{ 'invoice.number'|trans({}, 'messages', language) }}: {{ model.numberGenerator.invoiceNumber }}
{{ 'label.orderNumber'|trans }}{{ 'label.orderNumber'|trans({}, 'messages', language) }} {{ model.query.project.orderNumber }}
{{ 'label.vat_id'|trans }}:{{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.template.vatId }}
-

{{ model.template.title|default('timesheet'|trans({}, 'invoice-renderer')) }}

-

- {{ 'label.invoice_salutation'|trans|nl2br }} -

+

{{ model.template.title }}

- - - - + + + + {% 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 @@
{{ '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) }}
- + - + - +
{{ '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 @@
- + - + - + {% if model.query.project is not empty and model.query.project.orderNumber is not empty %} - + @@ -57,12 +59,12 @@
{{ '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 @@
{{ 'label.orderNumber'|trans }}{{ 'label.orderNumber'|trans({}, 'messages', language) }} {{ model.query.project.orderNumber }}
- + {% if model.query.user is empty %} - + {% endif %} - - + + @@ -79,7 +81,7 @@ {% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }} {% endif %} - + {% endfor %} @@ -89,8 +91,8 @@ {% if model.query.user is empty %} {% endif %} - - + +
{{ 'label.date'|trans }}{{ 'label.date'|trans({}, 'messages', language) }}{{ 'label.user'|trans }}{{ 'label.user'|trans({}, 'messages', language) }}{{ 'label.activity'|trans }}{{ 'label.hours'|trans }}{{ 'label.activity'|trans({}, 'messages', language) }}{{ 'label.hours'|trans({}, 'messages', language) }}
{{ entry.duration|duration }}{{ entry.duration|duration(isDecimal) }}
{{ '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: - -