Release 2.0.4 (#3883)

* fix column data truncated
* calculate internal rate from user
* show internal rate in timesheet listing
* Fixed: responsivenss and size of report start page icons
* fix: name display in dropdowns (and added tests)
* translate reload button
* fix invoice date might be in the past
* fail safe customer name handling
* translate invoice_date and invoice_date help
* prevent URLs like start=null
* prevent to reload select twice
This commit is contained in:
Kevin Papst
2023-03-02 14:04:06 +01:00
committed by GitHub
parent a8b972f8a5
commit 3f09a2674b
32 changed files with 409 additions and 77 deletions

View File

@@ -416,9 +416,10 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
const targetSelect = document.getElementById(apiSelect.dataset['relatedSelect']); const targetSelect = document.getElementById(apiSelect.dataset['relatedSelect']);
// if the related target select does not exist, we do not need to load the related data // if the related target select does not exist, we do not need to load the related data
if (targetSelect === null) { if (targetSelect === null || targetSelect.dataset['reloading'] === '1') {
return; return;
} }
targetSelect.dataset['reloading'] = '1';
if (targetSelect.tomselect !== undefined) { if (targetSelect.tomselect !== undefined) {
targetSelect.tomselect.disable(); targetSelect.tomselect.disable();
@@ -441,6 +442,7 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
if (selectValue === undefined || selectValue === null || selectValue === '' || (Array.isArray(selectValue) && selectValue.length === 0)) { if (selectValue === undefined || selectValue === null || selectValue === '' || (Array.isArray(selectValue) && selectValue.length === 0)) {
if (apiSelect.dataset['emptyUrl'] === undefined) { if (apiSelect.dataset['emptyUrl'] === undefined) {
this._updateSelect(targetSelectId, {}); this._updateSelect(targetSelectId, {});
targetSelect.dataset['reloading'] = '0';
return; return;
} }
newApiUrl = this._buildUrlWithFormFields(apiSelect.dataset['emptyUrl'], formPrefix); newApiUrl = this._buildUrlWithFormFields(apiSelect.dataset['emptyUrl'], formPrefix);
@@ -454,6 +456,7 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
if (targetSelect.tomselect !== undefined) { if (targetSelect.tomselect !== undefined) {
targetSelect.tomselect.enable(); targetSelect.tomselect.enable();
} }
targetSelect.dataset['reloading'] = '0';
targetSelect.disabled = false; targetSelect.disabled = false;
}); });
} }
@@ -533,10 +536,16 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
if (Array.isArray(newValue)) { if (Array.isArray(newValue)) {
let urlParams = []; let urlParams = [];
for (let tmpValue of newValue) { for (let tmpValue of newValue) {
if (tmpValue === null) {
tmpValue = '';
}
urlParams.push(originalFieldName + '=' + tmpValue); urlParams.push(originalFieldName + '=' + tmpValue);
} }
newApiUrl = newApiUrl.replace(item, urlParams.join('&')); newApiUrl = newApiUrl.replace(item, urlParams.join('&'));
} else { } else {
if (newValue === null) {
newValue = '';
}
newApiUrl = newApiUrl.replace(value, newValue); newApiUrl = newApiUrl.replace(value, newValue);
} }
} }

View File

@@ -1040,11 +1040,6 @@ parameters:
count: 2 count: 2
path: src/Controller/CalendarController.php path: src/Controller/CalendarController.php
-
message: "#^Parameter \\#1 \\$filename of static method App\\\\Utils\\\\FileHelper\\:\\:convertToAsciiFilename\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: src/Controller/CustomerController.php
- -
message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#" message: "#^Parameter \\#1 \\$name of class App\\\\Entity\\\\Team constructor expects string, string\\|null given\\.$#"
count: 1 count: 1
@@ -3395,11 +3390,6 @@ parameters:
count: 1 count: 1
path: src/Form/Helper/CustomerHelper.php path: src/Form/Helper/CustomerHelper.php
-
message: "#^Cannot call method getName\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
count: 1
path: src/Form/Helper/ProjectHelper.php
- -
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, bool\\|float\\|int\\|string given\\.$#" message: "#^Parameter \\#1 \\$haystack of function stripos expects string, bool\\|float\\|int\\|string given\\.$#"
count: 2 count: 2

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,10 +3,10 @@
"app": { "app": {
"js": [ "js": [
"/build/runtime.f0079159.js", "/build/runtime.f0079159.js",
"/build/app.46a6055b.js" "/build/app.952a9f98.js"
], ],
"css": [ "css": [
"/build/app.2c6c5920.css" "/build/app.c429ff5b.css"
] ]
}, },
"export-pdf": { "export-pdf": {
@@ -63,8 +63,8 @@
}, },
"integrity": { "integrity": {
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV", "/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
"/build/app.46a6055b.js": "sha384-4FR2SCr6YUmX48AWUg8rqFrfHSOTRj6tIqZJK/CjrNx0XV1km7nn2BCEVFNo0UkA", "/build/app.952a9f98.js": "sha384-qK3Hcem6As5Jy3ZiVz2rlABsJUIvNMQ0Dq6RfRBalbq1l88/zFfE5ZRXVMYtLP73",
"/build/app.2c6c5920.css": "sha384-qiubu7JONCsPTbVOb8bSJ4GYxioCwIUvlsI0usMmIUyoTxawn6jsjG7KK64iej5S", "/build/app.c429ff5b.css": "sha384-m7RJ9g2fW3tENrf+oi/FsH8wTu6fsjuhvpDztfqr+YjNN0AvCapER6yvmGkqwp+I",
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW", "/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg", "/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks", "/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks",

View File

@@ -1,6 +1,6 @@
{ {
"build/app.css": "/build/app.2c6c5920.css", "build/app.css": "/build/app.c429ff5b.css",
"build/app.js": "/build/app.46a6055b.js", "build/app.js": "/build/app.952a9f98.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css", "build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.587575e7.js", "build/export-pdf.js": "/build/export-pdf.587575e7.js",
"build/invoice.css": "/build/invoice.3c80ee80.css", "build/invoice.css": "/build/invoice.3c80ee80.css",

View File

@@ -17,11 +17,11 @@ class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '2.0.3'; public const VERSION = '2.0.4';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 20003; public const VERSION_ID = 20004;
/** /**
* The software name * The software name
*/ */

View File

@@ -241,7 +241,14 @@ abstract class AbstractController extends BaseAbstractController implements Serv
return substr($query->getName(), 0, 50); return substr($query->getName(), 0, 50);
} }
protected function handleSearch(FormInterface $form, Request $request): bool /**
* @param FormInterface $form
* @param Request $request
* @param array<string> $filterParams parameter names, which should not be saved (neither session, nor database)
* @return bool
* @throws \Exception
*/
protected function handleSearch(FormInterface $form, Request $request, array $filterParams = []): bool
{ {
$data = $form->getData(); $data = $form->getData();
if (!($data instanceof BaseQuery)) { if (!($data instanceof BaseQuery)) {
@@ -339,7 +346,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
} }
// these should NEVER be saved // these should NEVER be saved
$filter = ['setDefaultQuery', 'removeDefaultQuery', 'performSearch']; $filter = array_merge(['setDefaultQuery', 'removeDefaultQuery', 'performSearch'], $filterParams);
foreach ($filter as $name) { foreach ($filter as $name) {
if (isset($params[$name])) { if (isset($params[$name])) {
unset($params[$name]); unset($params[$name]);

View File

@@ -365,11 +365,15 @@ final class CustomerController extends AbstractController
#[IsGranted('view', 'customer')] #[IsGranted('view', 'customer')]
public function downloadVCard(Customer $customer): Response public function downloadVCard(Customer $customer): Response
{ {
if ($customer->getName() === null || \strlen($customer->getName()) === 0) {
throw new \Exception('Customer name cannot be null');
}
$vcard = new VCard(); $vcard = new VCard();
$contact = $customer->getContact() ?? $customer->getName(); $contact = $customer->getContact();
if ($contact === null || \strlen($contact) === 0) { if ($contact === null || \strlen($contact) === 0) {
$contact = 'Unknown'; $contact = $customer->getName();
} }
$contact = explode(' ', $contact); $contact = explode(' ', $contact);

View File

@@ -81,10 +81,13 @@ final class InvoiceController extends AbstractController
$query = $this->getDefaultQuery(); $query = $this->getDefaultQuery();
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
if ($this->handleSearch($form, $request)) { if ($this->handleSearch($form, $request, ['invoiceDate'])) {
return $this->redirectToRoute('invoice'); return $this->redirectToRoute('invoice');
} }
// this can be deleted in the future, but for now invalid bookmarks exists, which contain an old invoice date
$query->setInvoiceDate($this->getDateTimeFactory()->createDateTime());
$models = []; $models = [];
$total = 0; $total = 0;
$searched = false; $searched = false;
@@ -116,6 +119,8 @@ final class InvoiceController extends AbstractController
->add('template', InvoiceTemplateType::class) ->add('template', InvoiceTemplateType::class)
->add('invoiceDate', DatePickerType::class, [ ->add('invoiceDate', DatePickerType::class, [
'required' => true, 'required' => true,
'label' => 'invoice_date',
'help' => 'invoice_date.help'
]) ])
->createView(); ->createView();
} }

View File

@@ -88,6 +88,7 @@ abstract class TimesheetAbstractController extends AbstractController
if ($canSeeRate) { if ($canSeeRate) {
$table->addColumn('hourlyRate', ['class' => 'text-end d-none text-nowrap']); $table->addColumn('hourlyRate', ['class' => 'text-end d-none text-nowrap']);
$table->addColumn('internalRate', ['class' => 'text-end text-nowrap']);
$table->addColumn('rate', ['class' => 'text-end text-nowrap']); $table->addColumn('rate', ['class' => 'text-end text-nowrap']);
} }
@@ -113,6 +114,7 @@ abstract class TimesheetAbstractController extends AbstractController
$page->setActionName($this->getActionName()); $page->setActionName($this->getActionName());
return $this->render('timesheet/index.html.twig', [ return $this->render('timesheet/index.html.twig', [
'view_rate' => $canSeeRate,
'page_setup' => $page, 'page_setup' => $page,
'dataTable' => $table, 'dataTable' => $table,
'action_single' => $this->getActionNameSingle(), 'action_single' => $this->getActionNameSingle(),

View File

@@ -102,7 +102,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
* URL to the user avatar, will be auto-generated if empty * URL to the user avatar, will be auto-generated if empty
*/ */
#[ORM\Column(name: 'avatar', type: 'string', length: 255, nullable: true)] #[ORM\Column(name: 'avatar', type: 'string', length: 255, nullable: true)]
#[Assert\Length(max: 255)] #[Assert\Length(max: 255, groups: ['Profile'])]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['User_Entity'])] #[Serializer\Groups(['User_Entity'])]
private ?string $avatar = null; private ?string $avatar = null;

View File

@@ -46,7 +46,7 @@ final class InvoiceDocumentSubscriber extends AbstractActionsSubscriber
$event->addAction('download', ['url' => $this->path('admin_invoice_document_download', ['document' => $document->getId()])]); $event->addAction('download', ['url' => $this->path('admin_invoice_document_download', ['document' => $document->getId()])]);
if ($document->isTwig()) { if ($document->isTwig()) {
$event->addAction('Reload', ['url' => $this->path('admin_invoice_document_reload', ['document' => $document->getId()])]); $event->addAction('reload', ['url' => $this->path('admin_invoice_document_reload', ['document' => $document->getId()])]);
} }
if (!$inUse) { if (!$inUse) {

View File

@@ -46,8 +46,13 @@ final class ActivityHelper
$name = str_replace(self::PATTERN_NAME, $activity->getName(), $name); $name = str_replace(self::PATTERN_NAME, $activity->getName(), $name);
$name = str_replace(self::PATTERN_COMMENT, $activity->getComment() ?? '', $name); $name = str_replace(self::PATTERN_COMMENT, $activity->getComment() ?? '', $name);
$name = ltrim($name, self::SPACER); while (str_starts_with($name, self::SPACER)) {
$name = rtrim($name, self::SPACER); $name = substr($name, \strlen(self::SPACER));
}
while (str_ends_with($name, self::SPACER)) {
$name = substr($name, 0, -\strlen(self::SPACER));
}
if ($name === '' || $name === self::SPACER) { if ($name === '' || $name === self::SPACER) {
$name = $activity->getName(); $name = $activity->getName();

View File

@@ -50,8 +50,13 @@ final class CustomerHelper
$name = str_replace(self::PATTERN_NUMBER, $customer->getNumber() ?? '', $name); $name = str_replace(self::PATTERN_NUMBER, $customer->getNumber() ?? '', $name);
$name = str_replace(self::PATTERN_COMPANY, $customer->getCompany() ?? '', $name); $name = str_replace(self::PATTERN_COMPANY, $customer->getCompany() ?? '', $name);
$name = ltrim($name, self::SPACER); while (str_starts_with($name, self::SPACER)) {
$name = rtrim($name, self::SPACER); $name = substr($name, \strlen(self::SPACER));
}
while (str_ends_with($name, self::SPACER)) {
$name = substr($name, 0, -\strlen(self::SPACER));
}
if ($name === '' || $name === self::SPACER) { if ($name === '' || $name === self::SPACER) {
$name = $customer->getName(); $name = $customer->getName();

View File

@@ -12,9 +12,10 @@ namespace App\Form\Helper;
use App\Configuration\LocaleService; use App\Configuration\LocaleService;
use App\Configuration\SystemConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Project; use App\Entity\Project;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
final class ProjectHelper final class ProjectHelper implements LocaleAwareInterface
{ {
public const PATTERN_NAME = '{name}'; public const PATTERN_NAME = '{name}';
public const PATTERN_COMMENT = '{comment}'; public const PATTERN_COMMENT = '{comment}';
@@ -30,11 +31,22 @@ final class ProjectHelper
private ?string $pattern = null; private ?string $pattern = null;
private bool $showStart = false; private bool $showStart = false;
private bool $showEnd = false; private bool $showEnd = false;
private ?string $locale = null;
public function __construct(private SystemConfiguration $configuration, private LocaleService $localeService, private TranslatorInterface $translator) public function __construct(private SystemConfiguration $configuration, private LocaleService $localeService, private TranslatorInterface $translator)
{ {
} }
public function getLocale(): string
{
return $this->locale ?? \Locale::getDefault();
}
public function setLocale(string $locale): void
{
$this->locale = $locale;
}
public function getChoicePattern(): string public function getChoicePattern(): string
{ {
if ($this->pattern === null) { if ($this->pattern === null) {
@@ -56,26 +68,27 @@ final class ProjectHelper
$name = $this->getChoicePattern(); $name = $this->getChoicePattern();
$name = str_replace(self::PATTERN_NAME, $project->getName(), $name); $name = str_replace(self::PATTERN_NAME, $project->getName(), $name);
$name = str_replace(self::PATTERN_COMMENT, $project->getComment() ?? '', $name); $name = str_replace(self::PATTERN_COMMENT, $project->getComment() ?? '', $name);
$name = str_replace(self::PATTERN_CUSTOMER, $project->getCustomer()->getName() ?? '', $name); $name = str_replace(self::PATTERN_CUSTOMER, $project->getCustomer()?->getName() ?? '', $name);
$name = str_replace(self::PATTERN_ORDERNUMBER, $project->getOrderNumber() ?? '', $name); $name = str_replace(self::PATTERN_ORDERNUMBER, $project->getOrderNumber() ?? '', $name);
if ($this->dateFormatter === null) { if ($this->dateFormatter === null) {
$this->showStart = stripos($name, self::PATTERN_START) !== false; $this->showStart = stripos($name, self::PATTERN_START) !== false;
$this->showEnd = stripos($name, self::PATTERN_END) !== false; $this->showEnd = stripos($name, self::PATTERN_END) !== false;
$locale = $this->getLocale();
$this->dateFormatter = new \IntlDateFormatter( $this->dateFormatter = new \IntlDateFormatter(
\Locale::getDefault(), $locale,
\IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM,
\IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM,
date_default_timezone_get(), date_default_timezone_get(),
\IntlDateFormatter::GREGORIAN, \IntlDateFormatter::GREGORIAN,
$this->localeService->getDateFormat(\Locale::getDefault()) $this->localeService->getDateFormat($locale)
); );
} }
if ($this->showStart) { if ($this->showStart) {
$start = ''; $start = '';
if ($project->getStart() !== null) { if ($project->getStart() !== null) {
$start = $this->translator->trans('project_start') . ': ' . $this->dateFormatter->format($project->getStart()) . ' '; $start = $this->translator->trans('project_start') . ': ' . $this->dateFormatter->format($project->getStart());
} }
$name = str_replace(self::PATTERN_START, $start, $name); $name = str_replace(self::PATTERN_START, $start, $name);
} }
@@ -83,14 +96,18 @@ final class ProjectHelper
if ($this->showEnd) { if ($this->showEnd) {
$end = ''; $end = '';
if ($project->getEnd() !== null) { if ($project->getEnd() !== null) {
$end = ' ' . $this->translator->trans('project_end') . ': ' . $this->dateFormatter->format($project->getEnd()); $end = $this->translator->trans('project_end') . ': ' . $this->dateFormatter->format($project->getEnd());
} }
$name = str_replace(self::PATTERN_END, $end, $name); $name = str_replace(self::PATTERN_END, $end, $name);
} }
$name = ltrim($name, self::SPACER); while (str_starts_with($name, self::SPACER)) {
$name = rtrim($name, self::SPACER); $name = substr($name, \strlen(self::SPACER));
$name = str_replace('- ?-?', '', $name); }
while (str_ends_with($name, self::SPACER)) {
$name = substr($name, 0, -\strlen(self::SPACER));
}
if ($name === '' || $name === self::SPACER) { if ($name === '' || $name === self::SPACER) {
$name = $project->getName(); $name = $project->getName();

View File

@@ -39,13 +39,11 @@ final class RateService implements RateServiceInterface
if (null !== $rate) { if (null !== $rate) {
if ($rate->isFixed()) { if ($rate->isFixed()) {
$fixedRate ??= $rate->getRate(); $fixedRate ??= $rate->getRate();
$fixedInternalRate = $rate->getRate();
if (null !== $rate->getInternalRate()) { if (null !== $rate->getInternalRate()) {
$fixedInternalRate = $rate->getInternalRate(); $fixedInternalRate = $rate->getInternalRate();
} }
} else { } else {
$hourlyRate ??= $rate->getRate(); $hourlyRate ??= $rate->getRate();
$internalRate = $rate->getRate();
if (null !== $rate->getInternalRate()) { if (null !== $rate->getInternalRate()) {
$internalRate = $rate->getInternalRate(); $internalRate = $rate->getInternalRate();
} }

View File

@@ -7,18 +7,20 @@
{% endblock %} {% endblock %}
{% block main %} {% block main %}
<div class="row row-cards"> <div class="row row-deck row-cards">
{% for report in reports %} {% for report in reports %}
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-4 col-xxl-3">
<div class="card"> <a class="card card-link" href="{{ path(report.route) }}">
<div class="card-body p-4 text-center"> <div class="card-body d-flex align-items-center">
<span class="avatar avatar-xl mb-3 avatar-rounded"><i class="{{ report.reportIcon|icon(false, report.reportIcon) }}" style="color: {{ null|colorize(report.reportIcon) }}"></i></span> <div class="row">
<div class="col-auto">
<span class="avatar"><i class="icon {{ report.reportIcon|icon(false, report.reportIcon) }}" style="color: {{ null|colorize(report.reportIcon) }}"></i></span>
</div> </div>
<div class="d-flex"> <div class="col p-2">{{ report.label|trans({}, 'reporting') }}</div>
<a href="{{ path(report.route) }}" class="card-btn">{{ report.label|trans({}, 'reporting') }}</a>
</div> </div>
</div> </div>
</a>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -14,6 +14,7 @@
{% set day = null %} {% set day = null %}
{% set dayDuration = 0 %} {% set dayDuration = 0 %}
{% set dayRate = {} %} {% set dayRate = {} %}
{% set dayInternalRate = {} %}
{% set dayHourlyRate = 0 %} {% set dayHourlyRate = 0 %}
{% set lastEntry = null %} {% set lastEntry = null %}
@@ -22,10 +23,11 @@
{% set day = entry.begin|date_short %} {% set day = entry.begin|date_short %}
{% endif %} {% endif %}
{%- if showSummary and day is not same as(entry.begin|date_short) -%} {%- if showSummary and day is not same as(entry.begin|date_short) -%}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, sortedColumns, dataTable) }} {{ _self.summary(day, dayDuration, dayHourlyRate, dayInternalRate, dayRate, sortedColumns, dataTable) }}
{% set day = entry.begin|date_short %} {% set day = entry.begin|date_short %}
{% set dayDuration = 0 %} {% set dayDuration = 0 %}
{% set dayRate = {} %} {% set dayRate = {} %}
{% set dayInternalRate = {} %}
{% set dayHourlyRate = 0 %} {% set dayHourlyRate = 0 %}
{%- endif -%} {%- endif -%}
{%- set customerCurrency = entry.project.customer.currency -%} {%- set customerCurrency = entry.project.customer.currency -%}
@@ -41,7 +43,11 @@
{% if dayRate[customerCurrency] is not defined %} {% if dayRate[customerCurrency] is not defined %}
{% set dayRate = dayRate|merge({(customerCurrency): 0}) %} {% set dayRate = dayRate|merge({(customerCurrency): 0}) %}
{% endif %} {% endif %}
{% if dayInternalRate[customerCurrency] is not defined %}
{% set dayInternalRate = dayInternalRate|merge({(customerCurrency): 0}) %}
{% endif %}
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %} {% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
{% set dayInternalRate = dayInternalRate|merge({(customerCurrency): dayInternalRate[customerCurrency] + entry.internalRate}) %}
{%- endif -%} {%- endif -%}
{% if dayHourlyRate is not null %} {% if dayHourlyRate is not null %}
{% if dayHourlyRate == 0 %} {% if dayHourlyRate == 0 %}
@@ -54,7 +60,7 @@
{% set lastEntry = entry %} {% set lastEntry = entry %}
{% endfor %} {% endfor %}
{% if showSummary %} {% if showSummary %}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, sortedColumns, dataTable) }} {{ _self.summary(day, dayDuration, dayHourlyRate, dayInternalRate, dayRate, sortedColumns, dataTable) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@@ -107,11 +113,17 @@
{% elseif column == 'hourlyRate' %} {% elseif column == 'hourlyRate' %}
{{ entryHourlyRate }} {{ entryHourlyRate }}
{% elseif column == 'rate' %} {% elseif column == 'rate' %}
{% if not entry.end or not is_granted('view_rate', entry) %} {% if not entry.end or not view_rate %}
&dash; &dash;
{% else %} {% else %}
{{ entry.rate|money(customerCurrency) }} {{ entry.rate|money(customerCurrency) }}
{% endif %} {% endif %}
{% elseif column == 'internalRate' %}
{% if not entry.end or not view_rate %}
&dash;
{% else %}
{{ entry.internalRate|money(customerCurrency) }}
{% endif %}
{% elseif column == 'customer' %} {% elseif column == 'customer' %}
{{ widgets.label_customer(entry.project.customer) }} {{ widgets.label_customer(entry.project.customer) }}
{% elseif column == 'project' %} {% elseif column == 'project' %}
@@ -143,7 +155,7 @@
</td> </td>
{% endblock %} {% endblock %}
{% macro summary(day, duration, dayHourlyRate, dayRates, sortedColumns, dataTable) %} {% macro summary(day, duration, dayHourlyRate, dayInternalRates, dayRates, sortedColumns, dataTable) %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
<tr class="summary info"> <tr class="summary info">
{% for column, data in sortedColumns %} {% for column, data in sortedColumns %}
@@ -163,6 +175,13 @@
<br> <br>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% elseif column == 'internalRate' %}
{% for currency, rate in dayInternalRates %}
{{ rate|money(currency) }}
{% if not loop.last %}
<br>
{% endif %}
{% endfor %}
{% else %} {% else %}
{% endif %} {% endif %}

View File

@@ -25,7 +25,7 @@ class ReportingControllerTest extends ControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/reporting/'); $this->request($client, '/reporting/');
$nodes = $client->getCrawler()->filter('section.content div.card'); $nodes = $client->getCrawler()->filter('section.content div.row-cards a.card-link');
$this->assertCount(11, $nodes); $this->assertCount(11, $nodes);
} }
@@ -33,7 +33,7 @@ class ReportingControllerTest extends ControllerBaseTest
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->request($client, '/reporting/'); $this->request($client, '/reporting/');
$nodes = $client->getCrawler()->filter('section.content div.card'); $nodes = $client->getCrawler()->filter('section.content div.row-cards a.card-link');
$this->assertCount(3, $nodes); $this->assertCount(3, $nodes);
} }
} }

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Form\Helper;
use App\Entity\Activity;
use App\Form\Helper\ActivityHelper;
use App\Tests\Mocks\SystemConfigurationFactory;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Form\Helper\ActivityHelper
*/
class ActivityHelperTest extends TestCase
{
private function createSut(string $format): ActivityHelper
{
$config = SystemConfigurationFactory::createStub(['activity.choice_pattern' => $format]);
$helper = new ActivityHelper($config);
return $helper;
}
public function testInvalidPattern(): void
{
$helper = $this->createSut('sdfsdf');
self::assertEquals(ActivityHelper::PATTERN_NAME, $helper->getChoicePattern());
}
public function testGetChoicePattern(): void
{
$helper = $this->createSut(
ActivityHelper::PATTERN_NAME . ActivityHelper::PATTERN_SPACER .
ActivityHelper::PATTERN_COMMENT
);
self::assertEquals(
ActivityHelper::PATTERN_NAME . ActivityHelper::SPACER .
ActivityHelper::PATTERN_COMMENT,
$helper->getChoicePattern()
);
}
public function testGetChoiceLabel(): void
{
$helper = $this->createSut(ActivityHelper::PATTERN_NAME . ActivityHelper::PATTERN_SPACER . ActivityHelper::PATTERN_COMMENT);
$activity = new Activity();
$activity->setName(' - --- - -FOO BAR- --- - - - ');
self::assertEquals('--- - -FOO BAR- ---', $helper->getChoiceLabel($activity));
$activity->setName('FOO BAR');
$activity->setComment('Lorem Ipsum');
self::assertEquals('FOO BAR - Lorem Ipsum', $helper->getChoiceLabel($activity));
}
}

View File

@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Form\Helper;
use App\Entity\Customer;
use App\Form\Helper\CustomerHelper;
use App\Tests\Mocks\SystemConfigurationFactory;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Form\Helper\CustomerHelper
*/
class CustomerHelperTest extends TestCase
{
private function createSut(string $format): CustomerHelper
{
$config = SystemConfigurationFactory::createStub(['customer.choice_pattern' => $format]);
$helper = new CustomerHelper($config);
return $helper;
}
public function testInvalidPattern(): void
{
$helper = $this->createSut('sdfsdf');
self::assertEquals(CustomerHelper::PATTERN_NAME, $helper->getChoicePattern());
}
public function testGetChoicePattern(): void
{
$helper = $this->createSut(
CustomerHelper::PATTERN_NAME . CustomerHelper::PATTERN_SPACER .
CustomerHelper::PATTERN_COMMENT . CustomerHelper::PATTERN_SPACER .
CustomerHelper::PATTERN_COMPANY . CustomerHelper::PATTERN_SPACER .
CustomerHelper::PATTERN_NUMBER
);
self::assertEquals(
CustomerHelper::PATTERN_NAME . CustomerHelper::SPACER .
CustomerHelper::PATTERN_COMMENT . CustomerHelper::SPACER .
CustomerHelper::PATTERN_COMPANY . CustomerHelper::SPACER .
CustomerHelper::PATTERN_NUMBER,
$helper->getChoicePattern()
);
}
public function testGetChoiceLabel(): void
{
$helper = $this->createSut(
CustomerHelper::PATTERN_NAME . CustomerHelper::PATTERN_SPACER .
CustomerHelper::PATTERN_COMMENT . CustomerHelper::PATTERN_SPACER .
CustomerHelper::PATTERN_COMPANY . CustomerHelper::PATTERN_SPACER .
CustomerHelper::PATTERN_NUMBER
);
$customer = new Customer(' - --- - -FOO BAR- --- - - - ');
self::assertEquals('--- - -FOO BAR- ---', $helper->getChoiceLabel($customer));
$customer = new Customer('FOO BAR');
$customer->setComment('Lorem Ipsum');
self::assertEquals('FOO BAR - Lorem Ipsum', $helper->getChoiceLabel($customer));
$customer->setCompany('Acme University');
self::assertEquals('FOO BAR - Lorem Ipsum - Acme University', $helper->getChoiceLabel($customer));
$customer->setNumber('2023-0815');
self::assertEquals('FOO BAR - Lorem Ipsum - Acme University - 2023-0815', $helper->getChoiceLabel($customer));
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Form\Helper;
use App\Configuration\LocaleService;
use App\Entity\Customer;
use App\Entity\Project;
use App\Form\Helper\ProjectHelper;
use App\Tests\Mocks\SystemConfigurationFactory;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @covers \App\Form\Helper\ProjectHelper
*/
class ProjectHelperTest extends TestCase
{
private function createSut(string $format): ProjectHelper
{
$config = SystemConfigurationFactory::createStub(['project.choice_pattern' => $format]);
$localeService = new LocaleService(['en_US' => ['date' => 'dd.MM.y']]);
$translator = $this->createMock(TranslatorInterface::class);
$translator->method('trans')->willReturn('dating');
$helper = new ProjectHelper($config, $localeService, $translator);
$helper->setLocale('en_US');
return $helper;
}
public function testInvalidPattern(): void
{
$helper = $this->createSut('sdfsdf');
self::assertEquals(ProjectHelper::PATTERN_NAME, $helper->getChoicePattern());
}
public function testGetChoicePattern(): void
{
$helper = $this->createSut(
ProjectHelper::PATTERN_NAME . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_COMMENT . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_CUSTOMER . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_ORDERNUMBER . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_START . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_END . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_DATERANGE
);
self::assertEquals(
ProjectHelper::PATTERN_NAME . ProjectHelper::SPACER .
ProjectHelper::PATTERN_COMMENT . ProjectHelper::SPACER .
ProjectHelper::PATTERN_CUSTOMER . ProjectHelper::SPACER .
ProjectHelper::PATTERN_ORDERNUMBER . ProjectHelper::SPACER .
ProjectHelper::PATTERN_START . ProjectHelper::SPACER .
ProjectHelper::PATTERN_END . ProjectHelper::SPACER .
ProjectHelper::PATTERN_START . '-' . ProjectHelper::PATTERN_END,
$helper->getChoicePattern()
);
}
public function testGetChoiceLabel(): void
{
$helper = $this->createSut(
ProjectHelper::PATTERN_NAME . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_COMMENT . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_CUSTOMER . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_ORDERNUMBER
);
$project = new Project();
$project->setName(' - --- - -FOO BAR- --- - - - ');
$customer = new Customer(' - --- - - Acme company- --- - - - ');
$project->setCustomer($customer);
self::assertEquals('--- - -FOO BAR- --- - - - - - - --- - - Acme company- ---', $helper->getChoiceLabel($project));
$project = new Project();
$project->setName('FOO BAR');
$customer = new Customer('Acme company');
$project->setCustomer($customer);
$project->setComment('Lorem Ipsum');
$project->setOrderNumber('F76/123');
self::assertEquals('FOO BAR - Lorem Ipsum - Acme company - F76/123', $helper->getChoiceLabel($project));
}
public function testGetChoiceLabelWithDates(): void
{
$helper = $this->createSut(
ProjectHelper::PATTERN_NAME . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_START . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_END . ProjectHelper::PATTERN_SPACER .
ProjectHelper::PATTERN_DATERANGE
);
$project = new Project();
$project->setName('FOO BAR');
self::assertEquals('FOO BAR - - - -', $helper->getChoiceLabel($project));
$project->setStart(new \DateTime('2018-12-27 18:45:12'));
self::assertEquals('FOO BAR - dating: 27.12.2018 - - dating: 27.12.2018-', $helper->getChoiceLabel($project));
$project->setEnd(new \DateTime('2019-02-14 01:23:45'));
self::assertEquals('FOO BAR - dating: 27.12.2018 - dating: 14.02.2019 - dating: 27.12.2018-dating: 14.02.2019', $helper->getChoiceLabel($project));
}
}

View File

@@ -76,25 +76,25 @@ class RateCalculatorTest extends TestCase
yield 'a6' => [0.5, 6.72, 1800, 1, 13.44, null, null, null, null, false, null, null, false, null, null, false]; yield 'a6' => [0.5, 6.72, 1800, 1, 13.44, null, null, null, null, false, null, null, false, null, null, false];
yield 'a8' => [0.0, 1, 0, 0, 0, 0, 0, 0, 1, true, 0, null, true, 0, null, true]; yield 'a8' => [0.0, 1, 0, 0, 0, 0, 0, 0, 1, true, 0, null, true, 0, null, true];
// rate: 1.5 => timesheet hourly rate , internal: 2.5 => activity hourly rate (30 min) // rate: 1.5 => timesheet hourly rate , internal: 2.5 => activity hourly rate (30 min)
yield 'b1' => [1.5, 2.5, 1800, 1, 1, 3, null, 5, null, false, 7, null, false, 9, null, false]; yield 'b1' => [1.5, 0.5, 1800, 1, 1, 3, null, 5, null, false, 7, null, false, 9, null, false];
yield 'b2' => [2.5, 2.5, 1800, 1, 1, null, null, 5, null, false, 7, null, false, 9, null, false]; yield 'b2' => [2.5, 4.5, 1800, 1, 9, null, null, 5, null, false, 7, null, false, 9, null, false];
yield 'b3' => [3.5, 6.5, 1800, 1, 1, null, null, null, null, false, 7, 13, false, 9, 9, false]; yield 'b3' => [3.5, 6.5, 1800, 1, 1, null, null, null, null, false, 7, 13, false, 9, 9, false];
yield 'b4' => [4.5, 6.5, 1800, 1, 15, null, null, null, null, false, null, null, false, 9, 13, false]; yield 'b4' => [4.5, 6.5, 1800, 1, 15, null, null, null, null, false, null, null, false, 9, 13, false];
// rate: 2.0 => timesheet fixed rate , internal: 3.0 => activity fixed rate // rate: 2.0 => timesheet fixed rate , internal: 3.0 => activity fixed rate
yield 'b5' => [2.0, 3.0, 1800, 1, 1, null, 2, 3, null, true, 4, null, true, 5, null, true]; yield 'b5' => [2.0, 1.0, 1800, 1, 1, null, 2, 3, null, true, 4, null, true, 5, null, true];
yield 'b6' => [3.0, 3.0, 1800, 1, 1, null, null, 3, null, true, 4, null, true, 5, null, true]; yield 'b6' => [3.0, 3.0, 1800, 1, 3, null, null, 3, null, true, 4, null, true, 5, null, true];
yield 'b7' => [4.0, 4.0, 1800, 1, 1, null, null, null, null, false, 4, null, true, 5, null, true]; yield 'b7' => [4.0, 7.0, 1800, 1, 7, null, null, null, null, false, 4, null, true, 5, null, true];
yield 'b8' => [3.0, 3.0, 1800, 1, 1, null, null, 3, null, true, null, null, false, 5, null, true]; yield 'b8' => [3.0, 4.7, 1800, 1, 4.7, null, null, 3, null, true, null, null, false, 5, null, true];
// rate: 2.0 => timesheet fixed rate , internal: 5.0 => customer hourly rate // rate: 2.0 => timesheet fixed rate , internal: 5.0 => customer hourly rate
yield 'b9' => [2.0, 5.0, 1800, 1, 1, null, 2, null, null, false, null, null, false, 5, null, true]; yield 'b9' => [2.0, 71.0, 1800, 1, 71, null, 2, null, null, false, null, null, false, 5, null, true];
// rate: 5.0 => timesheet hourly rate , internal: 7.5 => user internal rate (30 min) // rate: 5.0 => timesheet hourly rate , internal: 7.5 => user internal rate (30 min)
yield 'c0' => [5.0, 7.5, 1800, 100, 15, 10, null, null, null, false, null, null, false, null, null, false]; yield 'c0' => [5.0, 7.5, 1800, 100, 15, 10, null, null, null, false, null, null, false, null, null, false];
// internal: 10 because no rule applies and as fallback the users internal rate is used // internal: 10 because no rule applies and as fallback the users internal rate is used
yield 'd0' => [10, 100, 1800, 100, 100, null, 10, null, null, false, null, null, false, null, null, false]; yield 'd0' => [10, 100, 1800, 100, 100, null, 10, null, null, false, null, null, false, null, null, false];
yield 'e0' => [10, 10, 1800, 100, 100, null, null, 20, null, false, null, null, false, null, null, false]; yield 'e0' => [10, 50, 1800, 100, 100, null, null, 20, null, false, null, null, false, null, null, false];
yield 'f0' => [20, 78, 1800, 100, 100, null, null, 20, 78, true, null, null, false, null, null, false]; yield 'f0' => [20, 78, 1800, 100, 100, null, null, 20, 78, true, null, null, false, null, null, false];
yield 'g0' => [15, 11.5, 1800, 100, 100, null, null, null, null, false, 30, 23, false, null, null, false]; yield 'g0' => [15, 11.5, 1800, 100, 100, null, null, null, null, false, 30, 23, false, null, null, false];
yield 'h0' => [30, 30, 1800, 100, 100, null, null, null, null, false, 30, null, true, null, null, false]; yield 'h0' => [30, 100, 1800, 100, 100, null, null, null, null, false, 30, null, true, null, null, false];
yield 'i0' => [20, 13.5, 1800, 100, 100, null, null, null, null, false, null, null, false, 40, 27, false]; yield 'i0' => [20, 13.5, 1800, 100, 100, null, null, null, null, false, null, null, false, 40, 27, false];
yield 'j0' => [40, 84, 1800, 100, 45, null, null, null, null, false, null, null, false, 40, 84, true]; yield 'j0' => [40, 84, 1800, 100, 45, null, null, null, null, false, null, null, false, 40, 84, true];
// make sure the last fallback for the internal rate is the users hourly rate // make sure the last fallback for the internal rate is the users hourly rate

View File

@@ -80,25 +80,25 @@ class RateServiceTest extends TestCase
yield 'a6' => [0.5, 6.72, 1800, 1, 13.44, null, null, null, null, false, null, null, false, null, null, false]; yield 'a6' => [0.5, 6.72, 1800, 1, 13.44, null, null, null, null, false, null, null, false, null, null, false];
yield 'a8' => [0.0, 1, 0, 0, 0, 0, 0, 0, 1, true, 0, null, true, 0, null, true]; yield 'a8' => [0.0, 1, 0, 0, 0, 0, 0, 0, 1, true, 0, null, true, 0, null, true];
// rate: 1.5 => timesheet hourly rate , internal: 2.5 => activity hourly rate (30 min) // rate: 1.5 => timesheet hourly rate , internal: 2.5 => activity hourly rate (30 min)
yield 'b1' => [1.5, 2.5, 1800, 1, 1, 3, null, 5, null, false, 7, null, false, 9, null, false]; yield 'b1' => [1.5, 0.5, 1800, 1, 1, 3, null, 5, null, false, 7, null, false, 9, null, false];
yield 'b2' => [2.5, 2.5, 1800, 1, 1, null, null, 5, null, false, 7, null, false, 9, null, false]; yield 'b2' => [2.5, 4.5, 1800, 1, 9, null, null, 5, null, false, 7, null, false, 9, null, false];
yield 'b3' => [3.5, 6.5, 1800, 1, 1, null, null, null, null, false, 7, 13, false, 9, 9, false]; yield 'b3' => [3.5, 6.5, 1800, 1, 1, null, null, null, null, false, 7, 13, false, 9, 9, false];
yield 'b4' => [4.5, 6.5, 1800, 1, 15, null, null, null, null, false, null, null, false, 9, 13, false]; yield 'b4' => [4.5, 6.5, 1800, 1, 15, null, null, null, null, false, null, null, false, 9, 13, false];
// rate: 2.0 => timesheet fixed rate , internal: 3.0 => activity fixed rate // rate: 2.0 => timesheet fixed rate , internal: 3.0 => activity fixed rate
yield 'b5' => [2.0, 3.0, 1800, 1, 1, null, 2, 3, null, true, 4, null, true, 5, null, true]; yield 'b5' => [2.0, 1.0, 1800, 1, 1, null, 2, 3, null, true, 4, null, true, 5, null, true];
yield 'b6' => [3.0, 3.0, 1800, 1, 1, null, null, 3, null, true, 4, null, true, 5, null, true]; yield 'b6' => [3.0, 3.0, 1800, 1, 3, null, null, 3, null, true, 4, null, true, 5, null, true];
yield 'b7' => [4.0, 4.0, 1800, 1, 1, null, null, null, null, false, 4, null, true, 5, null, true]; yield 'b7' => [4.0, 7.0, 1800, 1, 7, null, null, null, null, false, 4, null, true, 5, null, true];
yield 'b8' => [3.0, 3.0, 1800, 1, 1, null, null, 3, null, true, null, null, false, 5, null, true]; yield 'b8' => [3.0, 4.7, 1800, 1, 4.7, null, null, 3, null, true, null, null, false, 5, null, true];
// rate: 2.0 => timesheet fixed rate , internal: 5.0 => customer hourly rate // rate: 2.0 => timesheet fixed rate , internal: 5.0 => customer hourly rate
yield 'b9' => [2.0, 5.0, 1800, 1, 1, null, 2, null, null, false, null, null, false, 5, null, true]; yield 'b9' => [2.0, 71.0, 1800, 1, 71, null, 2, null, null, false, null, null, false, 5, null, true];
// rate: 5.0 => timesheet hourly rate , internal: 7.5 => user internal rate (30 min) // rate: 5.0 => timesheet hourly rate , internal: 7.5 => user internal rate (30 min)
yield 'c0' => [5.0, 7.5, 1800, 100, 15, 10, null, null, null, false, null, null, false, null, null, false]; yield 'c0' => [5.0, 7.5, 1800, 100, 15, 10, null, null, null, false, null, null, false, null, null, false];
// internal: 10 because no rule applies and as fallback the users internal rate is used // internal: 10 because no rule applies and as fallback the users internal rate is used
yield 'd0' => [10, 100, 1800, 100, 100, null, 10, null, null, false, null, null, false, null, null, false]; yield 'd0' => [10, 100, 1800, 100, 100, null, 10, null, null, false, null, null, false, null, null, false];
yield 'e0' => [10, 10, 1800, 100, 100, null, null, 20, null, false, null, null, false, null, null, false]; yield 'e0' => [10, 50, 1800, 100, 100, null, null, 20, null, false, null, null, false, null, null, false];
yield 'f0' => [20, 78, 1800, 100, 100, null, null, 20, 78, true, null, null, false, null, null, false]; yield 'f0' => [20, 78, 1800, 100, 100, null, null, 20, 78, true, null, null, false, null, null, false];
yield 'g0' => [15, 11.5, 1800, 100, 100, null, null, null, null, false, 30, 23, false, null, null, false]; yield 'g0' => [15, 11.5, 1800, 100, 100, null, null, null, null, false, 30, 23, false, null, null, false];
yield 'h0' => [30, 30, 1800, 100, 100, null, null, null, null, false, 30, null, true, null, null, false]; yield 'h0' => [30, 100, 1800, 100, 100, null, null, null, null, false, 30, null, true, null, null, false];
yield 'i0' => [20, 13.5, 1800, 100, 100, null, null, null, null, false, null, null, false, 40, 27, false]; yield 'i0' => [20, 13.5, 1800, 100, 100, null, null, null, null, false, null, null, false, 40, 27, false];
yield 'j0' => [40, 84, 1800, 100, 45, null, null, null, null, false, null, null, false, 40, 84, true]; yield 'j0' => [40, 84, 1800, 100, 45, null, null, null, null, false, null, null, false, 40, 84, true];
// make sure the last fallback for the internal rate is the users hourly rate // make sure the last fallback for the internal rate is the users hourly rate

View File

@@ -6,6 +6,10 @@
<source>stop</source> <source>stop</source>
<target>Beenden</target> <target>Beenden</target>
</trans-unit> </trans-unit>
<trans-unit id="QCf1FUG" resname="reload">
<source>reload</source>
<target>Neu laden</target>
</trans-unit>
<trans-unit id="PEgjRvN" resname="back"> <trans-unit id="PEgjRvN" resname="back">
<source>back</source> <source>back</source>
<target>Zurück</target> <target>Zurück</target>

View File

@@ -6,6 +6,10 @@
<source>stop</source> <source>stop</source>
<target>Stop</target> <target>Stop</target>
</trans-unit> </trans-unit>
<trans-unit id="QCf1FUG" resname="reload">
<source>reload</source>
<target>Reload</target>
</trans-unit>
<trans-unit id="PEgjRvN" resname="back"> <trans-unit id="PEgjRvN" resname="back">
<source>back</source> <source>back</source>
<target>Back</target> <target>Back</target>

View File

@@ -1081,6 +1081,14 @@
<source>invoice.service_date</source> <source>invoice.service_date</source>
<target>Leistungsdatum</target> <target>Leistungsdatum</target>
</trans-unit> </trans-unit>
<trans-unit id="NOEx3FF" resname="invoice_date.help">
<source>invoice_date.help</source>
<target>Hier können Sie das Rechnungsdatum ändern</target>
</trans-unit>
<trans-unit id="F7xeAn4" resname="invoice_date">
<source>invoice_date</source>
<target>Rechnungsdatum</target>
</trans-unit>
<trans-unit id="zzjZXJx" resname="amount"> <trans-unit id="zzjZXJx" resname="amount">
<source>amount</source> <source>amount</source>
<target>Anzahl</target> <target>Anzahl</target>

View File

@@ -1081,6 +1081,14 @@
<source>invoice.service_date</source> <source>invoice.service_date</source>
<target>Service date</target> <target>Service date</target>
</trans-unit> </trans-unit>
<trans-unit id="NOEx3FF" resname="invoice_date.help">
<source>invoice_date.help</source>
<target>You can change the invoice date here</target>
</trans-unit>
<trans-unit id="F7xeAn4" resname="invoice_date">
<source>invoice_date</source>
<target>Invoice date</target>
</trans-unit>
<trans-unit id="zzjZXJx" resname="amount"> <trans-unit id="zzjZXJx" resname="amount">
<source>amount</source> <source>amount</source>
<target>Quantity</target> <target>Quantity</target>