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:
@@ -416,9 +416,10 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
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 (targetSelect === null) {
|
||||
if (targetSelect === null || targetSelect.dataset['reloading'] === '1') {
|
||||
return;
|
||||
}
|
||||
targetSelect.dataset['reloading'] = '1';
|
||||
|
||||
if (targetSelect.tomselect !== undefined) {
|
||||
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 (apiSelect.dataset['emptyUrl'] === undefined) {
|
||||
this._updateSelect(targetSelectId, {});
|
||||
targetSelect.dataset['reloading'] = '0';
|
||||
return;
|
||||
}
|
||||
newApiUrl = this._buildUrlWithFormFields(apiSelect.dataset['emptyUrl'], formPrefix);
|
||||
@@ -454,6 +456,7 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
if (targetSelect.tomselect !== undefined) {
|
||||
targetSelect.tomselect.enable();
|
||||
}
|
||||
targetSelect.dataset['reloading'] = '0';
|
||||
targetSelect.disabled = false;
|
||||
});
|
||||
}
|
||||
@@ -533,10 +536,16 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
if (Array.isArray(newValue)) {
|
||||
let urlParams = [];
|
||||
for (let tmpValue of newValue) {
|
||||
if (tmpValue === null) {
|
||||
tmpValue = '';
|
||||
}
|
||||
urlParams.push(originalFieldName + '=' + tmpValue);
|
||||
}
|
||||
newApiUrl = newApiUrl.replace(item, urlParams.join('&'));
|
||||
} else {
|
||||
if (newValue === null) {
|
||||
newValue = '';
|
||||
}
|
||||
newApiUrl = newApiUrl.replace(value, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
10
phpstan.neon
10
phpstan.neon
@@ -1040,11 +1040,6 @@ parameters:
|
||||
count: 2
|
||||
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\\.$#"
|
||||
count: 1
|
||||
@@ -3395,11 +3390,6 @@ parameters:
|
||||
count: 1
|
||||
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\\.$#"
|
||||
count: 2
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/app.952a9f98.js
Normal file
2
public/build/app.952a9f98.js
Normal file
File diff suppressed because one or more lines are too long
1
public/build/app.c429ff5b.css
Normal file
1
public/build/app.c429ff5b.css
Normal file
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/app.46a6055b.js"
|
||||
"/build/app.952a9f98.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.2c6c5920.css"
|
||||
"/build/app.c429ff5b.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -63,8 +63,8 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||
"/build/app.46a6055b.js": "sha384-4FR2SCr6YUmX48AWUg8rqFrfHSOTRj6tIqZJK/CjrNx0XV1km7nn2BCEVFNo0UkA",
|
||||
"/build/app.2c6c5920.css": "sha384-qiubu7JONCsPTbVOb8bSJ4GYxioCwIUvlsI0usMmIUyoTxawn6jsjG7KK64iej5S",
|
||||
"/build/app.952a9f98.js": "sha384-qK3Hcem6As5Jy3ZiVz2rlABsJUIvNMQ0Dq6RfRBalbq1l88/zFfE5ZRXVMYtLP73",
|
||||
"/build/app.c429ff5b.css": "sha384-m7RJ9g2fW3tENrf+oi/FsH8wTu6fsjuhvpDztfqr+YjNN0AvCapER6yvmGkqwp+I",
|
||||
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build/app.css": "/build/app.2c6c5920.css",
|
||||
"build/app.js": "/build/app.46a6055b.js",
|
||||
"build/app.css": "/build/app.c429ff5b.css",
|
||||
"build/app.js": "/build/app.952a9f98.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
||||
"build/invoice.css": "/build/invoice.3c80ee80.css",
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.3';
|
||||
public const VERSION = '2.0.4';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20003;
|
||||
public const VERSION_ID = 20004;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -241,7 +241,14 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
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();
|
||||
if (!($data instanceof BaseQuery)) {
|
||||
@@ -339,7 +346,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
}
|
||||
|
||||
// these should NEVER be saved
|
||||
$filter = ['setDefaultQuery', 'removeDefaultQuery', 'performSearch'];
|
||||
$filter = array_merge(['setDefaultQuery', 'removeDefaultQuery', 'performSearch'], $filterParams);
|
||||
foreach ($filter as $name) {
|
||||
if (isset($params[$name])) {
|
||||
unset($params[$name]);
|
||||
|
||||
@@ -365,11 +365,15 @@ final class CustomerController extends AbstractController
|
||||
#[IsGranted('view', 'customer')]
|
||||
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();
|
||||
|
||||
$contact = $customer->getContact() ?? $customer->getName();
|
||||
$contact = $customer->getContact();
|
||||
if ($contact === null || \strlen($contact) === 0) {
|
||||
$contact = 'Unknown';
|
||||
$contact = $customer->getName();
|
||||
}
|
||||
|
||||
$contact = explode(' ', $contact);
|
||||
|
||||
@@ -81,10 +81,13 @@ final class InvoiceController extends AbstractController
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
if ($this->handleSearch($form, $request, ['invoiceDate'])) {
|
||||
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 = [];
|
||||
$total = 0;
|
||||
$searched = false;
|
||||
@@ -116,6 +119,8 @@ final class InvoiceController extends AbstractController
|
||||
->add('template', InvoiceTemplateType::class)
|
||||
->add('invoiceDate', DatePickerType::class, [
|
||||
'required' => true,
|
||||
'label' => 'invoice_date',
|
||||
'help' => 'invoice_date.help'
|
||||
])
|
||||
->createView();
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
if ($canSeeRate) {
|
||||
$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']);
|
||||
}
|
||||
|
||||
@@ -113,6 +114,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$page->setActionName($this->getActionName());
|
||||
|
||||
return $this->render('timesheet/index.html.twig', [
|
||||
'view_rate' => $canSeeRate,
|
||||
'page_setup' => $page,
|
||||
'dataTable' => $table,
|
||||
'action_single' => $this->getActionNameSingle(),
|
||||
|
||||
@@ -102,7 +102,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
* URL to the user avatar, will be auto-generated if empty
|
||||
*/
|
||||
#[ORM\Column(name: 'avatar', type: 'string', length: 255, nullable: true)]
|
||||
#[Assert\Length(max: 255)]
|
||||
#[Assert\Length(max: 255, groups: ['Profile'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
private ?string $avatar = null;
|
||||
|
||||
@@ -46,7 +46,7 @@ final class InvoiceDocumentSubscriber extends AbstractActionsSubscriber
|
||||
$event->addAction('download', ['url' => $this->path('admin_invoice_document_download', ['document' => $document->getId()])]);
|
||||
|
||||
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) {
|
||||
|
||||
@@ -46,8 +46,13 @@ final class ActivityHelper
|
||||
$name = str_replace(self::PATTERN_NAME, $activity->getName(), $name);
|
||||
$name = str_replace(self::PATTERN_COMMENT, $activity->getComment() ?? '', $name);
|
||||
|
||||
$name = ltrim($name, self::SPACER);
|
||||
$name = rtrim($name, self::SPACER);
|
||||
while (str_starts_with($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) {
|
||||
$name = $activity->getName();
|
||||
|
||||
@@ -50,8 +50,13 @@ final class CustomerHelper
|
||||
$name = str_replace(self::PATTERN_NUMBER, $customer->getNumber() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMPANY, $customer->getCompany() ?? '', $name);
|
||||
|
||||
$name = ltrim($name, self::SPACER);
|
||||
$name = rtrim($name, self::SPACER);
|
||||
while (str_starts_with($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) {
|
||||
$name = $customer->getName();
|
||||
|
||||
@@ -12,9 +12,10 @@ namespace App\Form\Helper;
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Project;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class ProjectHelper
|
||||
final class ProjectHelper implements LocaleAwareInterface
|
||||
{
|
||||
public const PATTERN_NAME = '{name}';
|
||||
public const PATTERN_COMMENT = '{comment}';
|
||||
@@ -30,11 +31,22 @@ final class ProjectHelper
|
||||
private ?string $pattern = null;
|
||||
private bool $showStart = false;
|
||||
private bool $showEnd = false;
|
||||
private ?string $locale = null;
|
||||
|
||||
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
|
||||
{
|
||||
if ($this->pattern === null) {
|
||||
@@ -56,26 +68,27 @@ final class ProjectHelper
|
||||
$name = $this->getChoicePattern();
|
||||
$name = str_replace(self::PATTERN_NAME, $project->getName(), $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);
|
||||
|
||||
if ($this->dateFormatter === null) {
|
||||
$this->showStart = stripos($name, self::PATTERN_START) !== false;
|
||||
$this->showEnd = stripos($name, self::PATTERN_END) !== false;
|
||||
$locale = $this->getLocale();
|
||||
$this->dateFormatter = new \IntlDateFormatter(
|
||||
\Locale::getDefault(),
|
||||
$locale,
|
||||
\IntlDateFormatter::MEDIUM,
|
||||
\IntlDateFormatter::MEDIUM,
|
||||
date_default_timezone_get(),
|
||||
\IntlDateFormatter::GREGORIAN,
|
||||
$this->localeService->getDateFormat(\Locale::getDefault())
|
||||
$this->localeService->getDateFormat($locale)
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->showStart) {
|
||||
$start = '';
|
||||
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);
|
||||
}
|
||||
@@ -83,14 +96,18 @@ final class ProjectHelper
|
||||
if ($this->showEnd) {
|
||||
$end = '';
|
||||
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 = ltrim($name, self::SPACER);
|
||||
$name = rtrim($name, self::SPACER);
|
||||
$name = str_replace('- ?-?', '', $name);
|
||||
while (str_starts_with($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) {
|
||||
$name = $project->getName();
|
||||
|
||||
@@ -39,13 +39,11 @@ final class RateService implements RateServiceInterface
|
||||
if (null !== $rate) {
|
||||
if ($rate->isFixed()) {
|
||||
$fixedRate ??= $rate->getRate();
|
||||
$fixedInternalRate = $rate->getRate();
|
||||
if (null !== $rate->getInternalRate()) {
|
||||
$fixedInternalRate = $rate->getInternalRate();
|
||||
}
|
||||
} else {
|
||||
$hourlyRate ??= $rate->getRate();
|
||||
$internalRate = $rate->getRate();
|
||||
if (null !== $rate->getInternalRate()) {
|
||||
$internalRate = $rate->getInternalRate();
|
||||
}
|
||||
|
||||
@@ -7,18 +7,20 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row row-cards">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
{% for report in reports %}
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 text-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="col-md-6 col-lg-4 col-xxl-3">
|
||||
<a class="card card-link" href="{{ path(report.route) }}">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<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 class="col p-2">{{ report.label|trans({}, 'reporting') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a href="{{ path(report.route) }}" class="card-btn">{{ report.label|trans({}, 'reporting') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
{% set day = null %}
|
||||
{% set dayDuration = 0 %}
|
||||
{% set dayRate = {} %}
|
||||
{% set dayInternalRate = {} %}
|
||||
{% set dayHourlyRate = 0 %}
|
||||
{% set lastEntry = null %}
|
||||
|
||||
@@ -22,10 +23,11 @@
|
||||
{% set day = entry.begin|date_short %}
|
||||
{% endif %}
|
||||
{%- 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 dayDuration = 0 %}
|
||||
{% set dayRate = {} %}
|
||||
{% set dayInternalRate = {} %}
|
||||
{% set dayHourlyRate = 0 %}
|
||||
{%- endif -%}
|
||||
{%- set customerCurrency = entry.project.customer.currency -%}
|
||||
@@ -41,7 +43,11 @@
|
||||
{% if dayRate[customerCurrency] is not defined %}
|
||||
{% set dayRate = dayRate|merge({(customerCurrency): 0}) %}
|
||||
{% endif %}
|
||||
{% if dayInternalRate[customerCurrency] is not defined %}
|
||||
{% set dayInternalRate = dayInternalRate|merge({(customerCurrency): 0}) %}
|
||||
{% endif %}
|
||||
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
|
||||
{% set dayInternalRate = dayInternalRate|merge({(customerCurrency): dayInternalRate[customerCurrency] + entry.internalRate}) %}
|
||||
{%- endif -%}
|
||||
{% if dayHourlyRate is not null %}
|
||||
{% if dayHourlyRate == 0 %}
|
||||
@@ -54,7 +60,7 @@
|
||||
{% set lastEntry = entry %}
|
||||
{% endfor %}
|
||||
{% if showSummary %}
|
||||
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, sortedColumns, dataTable) }}
|
||||
{{ _self.summary(day, dayDuration, dayHourlyRate, dayInternalRate, dayRate, sortedColumns, dataTable) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -107,11 +113,17 @@
|
||||
{% elseif column == 'hourlyRate' %}
|
||||
{{ entryHourlyRate }}
|
||||
{% elseif column == 'rate' %}
|
||||
{% if not entry.end or not is_granted('view_rate', entry) %}
|
||||
{% if not entry.end or not view_rate %}
|
||||
‐
|
||||
{% else %}
|
||||
{{ entry.rate|money(customerCurrency) }}
|
||||
{% endif %}
|
||||
{% elseif column == 'internalRate' %}
|
||||
{% if not entry.end or not view_rate %}
|
||||
‐
|
||||
{% else %}
|
||||
{{ entry.internalRate|money(customerCurrency) }}
|
||||
{% endif %}
|
||||
{% elseif column == 'customer' %}
|
||||
{{ widgets.label_customer(entry.project.customer) }}
|
||||
{% elseif column == 'project' %}
|
||||
@@ -143,7 +155,7 @@
|
||||
</td>
|
||||
{% 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 %}
|
||||
<tr class="summary info">
|
||||
{% for column, data in sortedColumns %}
|
||||
@@ -163,6 +175,13 @@
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% elseif column == 'internalRate' %}
|
||||
{% for currency, rate in dayInternalRates %}
|
||||
{{ rate|money(currency) }}
|
||||
{% if not loop.last %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -25,7 +25,7 @@ class ReportingControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class ReportingControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
62
tests/Form/Helper/ActivityHelperTest.php
Normal file
62
tests/Form/Helper/ActivityHelperTest.php
Normal 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));
|
||||
}
|
||||
}
|
||||
74
tests/Form/Helper/CustomerHelperTest.php
Normal file
74
tests/Form/Helper/CustomerHelperTest.php
Normal 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));
|
||||
}
|
||||
}
|
||||
109
tests/Form/Helper/ProjectHelperTest.php
Normal file
109
tests/Form/Helper/ProjectHelperTest.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\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));
|
||||
}
|
||||
}
|
||||
@@ -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 '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)
|
||||
yield 'b1' => [1.5, 2.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 'b1' => [1.5, 0.5, 1800, 1, 1, 3, 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 '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
|
||||
yield 'b5' => [2.0, 3.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 'b7' => [4.0, 4.0, 1800, 1, 1, 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 '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, 3, null, null, 3, null, true, 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, 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
|
||||
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)
|
||||
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
|
||||
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 '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 '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
|
||||
|
||||
@@ -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 '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)
|
||||
yield 'b1' => [1.5, 2.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 'b1' => [1.5, 0.5, 1800, 1, 1, 3, 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 '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
|
||||
yield 'b5' => [2.0, 3.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 'b7' => [4.0, 4.0, 1800, 1, 1, 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 '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, 3, null, null, 3, null, true, 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, 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
|
||||
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)
|
||||
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
|
||||
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 '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 '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
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<source>stop</source>
|
||||
<target>Beenden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="QCf1FUG" resname="reload">
|
||||
<source>reload</source>
|
||||
<target>Neu laden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="PEgjRvN" resname="back">
|
||||
<source>back</source>
|
||||
<target>Zurück</target>
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<source>stop</source>
|
||||
<target>Stop</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="QCf1FUG" resname="reload">
|
||||
<source>reload</source>
|
||||
<target>Reload</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="PEgjRvN" resname="back">
|
||||
<source>back</source>
|
||||
<target>Back</target>
|
||||
|
||||
@@ -1081,6 +1081,14 @@
|
||||
<source>invoice.service_date</source>
|
||||
<target>Leistungsdatum</target>
|
||||
</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">
|
||||
<source>amount</source>
|
||||
<target>Anzahl</target>
|
||||
|
||||
@@ -1081,6 +1081,14 @@
|
||||
<source>invoice.service_date</source>
|
||||
<target>Service date</target>
|
||||
</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">
|
||||
<source>amount</source>
|
||||
<target>Quantity</target>
|
||||
|
||||
Reference in New Issue
Block a user