Compare commits
14 Commits
docker-ips
...
1.x
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c341d340e6 | ||
|
|
777e3ec008 | ||
|
|
6df135d547 | ||
|
|
a5109781fa | ||
|
|
2d809b4f06 | ||
|
|
ced1883b6f | ||
|
|
2e54a9a267 | ||
|
|
57722f25fd | ||
|
|
539a7e280c | ||
|
|
db4179df19 | ||
|
|
7ea3eb1e3c | ||
|
|
559868fede | ||
|
|
d74236d9ac | ||
|
|
30bc3653d7 |
@@ -39,6 +39,7 @@ require('select2/dist/js/i18n/ru');
|
||||
require('select2/dist/js/i18n/sk');
|
||||
require('select2/dist/js/i18n/sv');
|
||||
require('select2/dist/js/i18n/tr');
|
||||
require('select2/dist/js/i18n/uk');
|
||||
require('select2/dist/js/i18n/vi');
|
||||
require('select2/dist/js/i18n/zh-CN');
|
||||
|
||||
@@ -74,6 +75,7 @@ require('moment/locale/ru');
|
||||
require('moment/locale/sk');
|
||||
require('moment/locale/sv');
|
||||
require('moment/locale/tr');
|
||||
require('moment/locale/uk');
|
||||
require('moment/locale/vi');
|
||||
require('moment/locale/zh-cn');
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ require('fullcalendar/dist/locale/ru');
|
||||
require('fullcalendar/dist/locale/sk');
|
||||
require('fullcalendar/dist/locale/sv');
|
||||
require('fullcalendar/dist/locale/tr');
|
||||
require('fullcalendar/dist/locale/uk');
|
||||
require('fullcalendar/dist/locale/zh-cn');
|
||||
require('fullcalendar/dist/locale/vi');
|
||||
require('fullcalendar/dist/locale/en-gb'); // the last imported file is used as fallback for locales that do not exist (like EO)
|
||||
|
||||
@@ -86,8 +86,8 @@
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpstan/phpstan-doctrine": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpstan/phpstan-symfony": "^1.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"phpstan/phpstan-symfony": "1.2.18",
|
||||
"phpunit/phpunit": "9.5.*",
|
||||
"symfony/browser-kit": "^4.4",
|
||||
"symfony/css-selector": "^4.4",
|
||||
"symfony/debug-bundle": "^4.4",
|
||||
|
||||
945
composer.lock
generated
945
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -17,3 +17,11 @@ monolog:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
deprecation:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
|
||||
deprecation_filter:
|
||||
type: filter
|
||||
handler: deprecation
|
||||
max_level: info
|
||||
channels: ["php"]
|
||||
|
||||
@@ -5,3 +5,11 @@ monolog:
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
deprecation:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
|
||||
deprecation_filter:
|
||||
type: filter
|
||||
handler: deprecation
|
||||
max_level: info
|
||||
channels: ["php"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: ar|cs|da|de|de_AT|de_CH|el|en|en_GB|eo|es|eu|fa|fi|fo|fr|he|hr|hu|it|ja|ko|nb_NO|nl|pl|pt|pt_BR|ro|ru|sk|sv|tr|vi|zh_CN
|
||||
app_locales: ar|cs|da|de|de_AT|de_CH|el|en|en_GB|eo|es|eu|fa|fi|fo|fr|he|hr|hu|it|ja|ko|nb_NO|nl|pl|pt|pt_BR|ro|ru|sk|sv|tr|uk|vi|zh_CN
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
"jquery-ui": "^1.12",
|
||||
"js-cookie": "^2.2",
|
||||
"moment": "^2.29",
|
||||
"node-sass": "^7.0",
|
||||
"sass-loader": "^11.0",
|
||||
"node-sass": "^8.0",
|
||||
"sass-loader": "^13.2",
|
||||
"select2": "^4.0",
|
||||
"select2-bootstrap-theme": "^0.1.0-beta.10",
|
||||
"sweetalert2": "^10.16",
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
public/build/app.adae4b3b.js
Normal file
2
public/build/app.adae4b3b.js
Normal file
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/calendar.d31fcf6a.js
Normal file
2
public/build/calendar.d31fcf6a.js
Normal file
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"build/runtime.b8e7bb04.js",
|
||||
"build/app.74a74ff6.js"
|
||||
"build/app.adae4b3b.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.dd7cccf4.css"
|
||||
@@ -36,7 +36,7 @@
|
||||
"calendar": {
|
||||
"js": [
|
||||
"build/runtime.b8e7bb04.js",
|
||||
"build/calendar.32dd2acd.js"
|
||||
"build/calendar.d31fcf6a.js"
|
||||
],
|
||||
"css": [
|
||||
"build/calendar.25e7e872.css"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"build/app.css": "build/app.dd7cccf4.css",
|
||||
"build/app.js": "build/app.74a74ff6.js",
|
||||
"build/app.js": "build/app.adae4b3b.js",
|
||||
"build/invoice.css": "build/invoice.ccdecd42.css",
|
||||
"build/invoice.js": "build/invoice.19f36eca.js",
|
||||
"build/invoice-pdf.css": "build/invoice-pdf.e73a6dda.css",
|
||||
"build/invoice-pdf.js": "build/invoice-pdf.0ce1f3ce.js",
|
||||
"build/chart.js": "build/chart.e845e930.js",
|
||||
"build/calendar.css": "build/calendar.25e7e872.css",
|
||||
"build/calendar.js": "build/calendar.32dd2acd.js",
|
||||
"build/calendar.js": "build/calendar.d31fcf6a.js",
|
||||
"build/runtime.js": "build/runtime.b8e7bb04.js",
|
||||
"build/images/fa-solid-900.svg": "build/images/fa-solid-900.13de59f1.svg",
|
||||
"build/images/fa-brands-400.svg": "build/images/fa-brands-400.216edb96.svg",
|
||||
|
||||
@@ -21,6 +21,7 @@ use App\Form\API\TimesheetApiEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use App\Utils\SearchTerm;
|
||||
@@ -75,19 +76,22 @@ class TimesheetController extends BaseApiController
|
||||
* @var TimesheetService
|
||||
*/
|
||||
private $service;
|
||||
private $userRepository;
|
||||
|
||||
public function __construct(
|
||||
ViewHandlerInterface $viewHandler,
|
||||
TimesheetRepository $repository,
|
||||
TagRepository $tagRepository,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
TimesheetService $service
|
||||
TimesheetService $service,
|
||||
UserRepository $userRepository
|
||||
) {
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->tagRepository = $tagRepository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->service = $service;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
protected function getTrackingMode(): TrackingModeInterface
|
||||
@@ -491,9 +495,13 @@ class TimesheetController extends BaseApiController
|
||||
|
||||
if ($this->isGranted('view_other_timesheet') && null !== ($reqUser = $paramFetcher->get('user'))) {
|
||||
if ('all' === $reqUser) {
|
||||
$reqUser = null;
|
||||
$user = null;
|
||||
} else {
|
||||
$user = $this->userRepository->getUserById($reqUser);
|
||||
if ($user === null) {
|
||||
throw $this->createNotFoundException('Unknown User ID');
|
||||
}
|
||||
}
|
||||
$user = $reqUser;
|
||||
}
|
||||
|
||||
if (null !== ($reqLimit = $paramFetcher->get('size'))) {
|
||||
|
||||
@@ -104,6 +104,10 @@ class ActivityService
|
||||
|
||||
public function findActivityByName(string $name, ?Project $project = null): ?Activity
|
||||
{
|
||||
if ($project === null) {
|
||||
return $this->repository->findOneBy(['project' => null, 'name' => $name]);
|
||||
}
|
||||
|
||||
return $this->repository->findOneBy(['project' => $project->getId(), 'name' => $name]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class ExportCreateCommand extends Command
|
||||
->addOption('customer', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Customer IDs to filter', null)
|
||||
->addOption('project', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Project IDs to filter', null)
|
||||
->addOption('team', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Team IDs to filter', null)
|
||||
->addOption('user', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'User IDs to filter', null)
|
||||
->addOption('user', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'User IDs to filter ("username" needs to be in the same Team)', null)
|
||||
->addOption('set-exported', null, InputOption::VALUE_NONE, 'Whether the included items should be marked as exported (default: false)')
|
||||
->addOption('template', null, InputOption::VALUE_REQUIRED, 'Export template', null)
|
||||
->addOption('exported', null, InputOption::VALUE_OPTIONAL, 'Exported filter for export entries. By default only "not exported" items are fetched (possible values: exported, all)', null)
|
||||
|
||||
@@ -1579,7 +1579,7 @@ final class KimaiImporterCommand extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($oldRecord['end']) || $oldRecord['end'] === 0) {
|
||||
if (empty($oldRecord['end'])) {
|
||||
$io->error('Cannot import running timesheet record, skipping: ' . $oldRecord['timeEntryID']);
|
||||
$failed++;
|
||||
continue;
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '1.30.1';
|
||||
public const VERSION = '1.30.11';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 13001;
|
||||
public const VERSION_ID = 13011;
|
||||
/**
|
||||
* The current release status, either "stable" or "dev"
|
||||
*/
|
||||
|
||||
@@ -44,6 +44,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* Controller used to create invoices and manage invoice templates.
|
||||
@@ -409,7 +410,7 @@ final class InvoiceController extends AbstractController
|
||||
* @Route(path="/document_upload", name="admin_invoice_document_upload", methods={"GET", "POST"})
|
||||
* @Security("is_granted('upload_invoice_template')")
|
||||
*/
|
||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository)
|
||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
$dir = $documentRepository->getUploadDirectory();
|
||||
$invoiceDir = $dir;
|
||||
@@ -418,6 +419,7 @@ final class InvoiceController extends AbstractController
|
||||
if ($invoiceDir[0] !== '/') {
|
||||
$invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir;
|
||||
}
|
||||
$invoiceDir = rtrim($invoiceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
|
||||
$used = [];
|
||||
foreach ($this->templateRepository->findAll() as $template) {
|
||||
@@ -473,23 +475,56 @@ final class InvoiceController extends AbstractController
|
||||
/** @var UploadedFile $uploadedFile */
|
||||
$uploadedFile = $form->get('document')->getData();
|
||||
|
||||
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
|
||||
$safeFilename = transliterator_transliterate(
|
||||
'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
|
||||
$originalFilename
|
||||
);
|
||||
$originalName = $uploadedFile->getClientOriginalName();
|
||||
$safeFilename = null;
|
||||
$extension = null;
|
||||
$success = true;
|
||||
|
||||
$extension = $uploadedFile->guessExtension();
|
||||
$allowed = InvoiceDocumentUploadForm::EXTENSIONS_NO_TWIG;
|
||||
if ((bool) $systemConfiguration->find('invoice.upload_twig') === true) {
|
||||
$allowed = InvoiceDocumentUploadForm::EXTENSIONS;
|
||||
}
|
||||
|
||||
$newFilename = substr($safeFilename, 0, 20) . '.' . $extension;
|
||||
foreach ($allowed as $ext) {
|
||||
$len = \strlen($ext);
|
||||
if (substr_compare($originalName, $ext, -$len) === 0) {
|
||||
$extension = $ext;
|
||||
$withoutExtension = str_replace($ext, '', $originalName);
|
||||
$safeFilename = transliterator_transliterate(InvoiceDocumentUploadForm::FILENAME_RULE, $withoutExtension);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$uploadedFile->move($invoiceDir, $newFilename);
|
||||
if ($safeFilename === null || $extension === null) {
|
||||
$success = false;
|
||||
$this->flashError('Invalid file given');
|
||||
} else {
|
||||
$newFilename = substr($safeFilename, 0, 20) . $extension;
|
||||
|
||||
try {
|
||||
$uploadedFile->move($invoiceDir, $newFilename);
|
||||
|
||||
// if this is a twig file, we directly try to compile the template
|
||||
if (stripos($newFilename, '.twig') !== false) {
|
||||
try {
|
||||
$twig->enableAutoReload();
|
||||
$twig->load('@invoice/' . $newFilename);
|
||||
$twig->disableAutoReload();
|
||||
} catch (Exception $ex) {
|
||||
unlink($invoiceDir . $newFilename);
|
||||
$success = false;
|
||||
$this->flashException($ex, 'File was deleted, as Twig template is broken: ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$this->flashException($ex, 'action.upload.error');
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_document_upload');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashException($ex, 'action.upload.error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +495,7 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setLabel('invoice.number_format')
|
||||
->setRequired(true)
|
||||
->setType(TextType::class)
|
||||
->setConstraints([new NotBlank()])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('invoice.simple_form')
|
||||
|
||||
@@ -148,14 +148,13 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
protected function create(Request $request, string $renderTemplate): Response
|
||||
{
|
||||
$entry = $this->service->createNewTimesheet($this->getUser());
|
||||
$entry = $this->service->createNewTimesheet($this->getUser(), $request);
|
||||
|
||||
$preForm = $this->createFormForGetRequest(TimesheetPreCreateForm::class, $entry, [
|
||||
'include_user' => $this->includeUserInForms('create'),
|
||||
]);
|
||||
$preForm->submit($request->query->all(), false);
|
||||
|
||||
$this->service->prepareNewTimesheet($entry, $request);
|
||||
$createForm = $this->getCreateForm($entry);
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
|
||||
@@ -95,14 +95,12 @@ class TeamFixtures extends Fixture
|
||||
$team->setName($faker->company . ' ' . $i);
|
||||
$team->addTeamlead($allUsers[array_rand($allUsers)]);
|
||||
|
||||
if ($userCount > 0) {
|
||||
$userKeys = array_rand($allUsers, $userCount);
|
||||
if (!\is_array($userKeys)) {
|
||||
$userKeys = [$userKeys];
|
||||
}
|
||||
foreach ($userKeys as $userKey) {
|
||||
$team->addUser($allUsers[$userKey]);
|
||||
}
|
||||
$userKeys = array_rand($allUsers, $userCount);
|
||||
if (!\is_array($userKeys)) {
|
||||
$userKeys = [$userKeys];
|
||||
}
|
||||
foreach ($userKeys as $userKey) {
|
||||
$team->addUser($allUsers[$userKey]);
|
||||
}
|
||||
|
||||
if ($projectCount > 0) {
|
||||
|
||||
@@ -333,6 +333,9 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('number_format')
|
||||
->defaultValue('{Y}/{cy,3}')
|
||||
->end()
|
||||
->booleanNode('upload_twig')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
|
||||
@@ -437,12 +437,21 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
{
|
||||
// only auto calculate if manually set duration is null - the result is important for eg. validations
|
||||
if ($calculate && $this->duration === null && $this->begin !== null && $this->end !== null) {
|
||||
return $this->end->getTimestamp() - $this->begin->getTimestamp();
|
||||
return $this->getCalculatedDuration();
|
||||
}
|
||||
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
public function getCalculatedDuration(): ?int
|
||||
{
|
||||
if ($this->begin !== null && $this->end !== null) {
|
||||
return $this->end->getTimestamp() - $this->begin->getTimestamp();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return Timesheet
|
||||
|
||||
@@ -45,6 +45,16 @@ class XlsxRenderer extends AbstractSpreadsheetRenderer
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
$this->applyStyles($spreadsheet);
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
protected function applyStyles(Spreadsheet $spreadsheet): void
|
||||
{
|
||||
// Store expensive calculations for later
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
@@ -75,11 +85,6 @@ class XlsxRenderer extends AbstractSpreadsheetRenderer
|
||||
->getAlignment()
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Repository\InvoiceDocumentRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
@@ -21,11 +22,18 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
class InvoiceDocumentUploadForm extends AbstractType
|
||||
{
|
||||
private $repository;
|
||||
public const EXTENSIONS = ['.html.twig', '.pdf.twig', '.docx', '.xlsx', '.ods'];
|
||||
public const EXTENSIONS_NO_TWIG = ['.docx', '.xlsx', '.ods'];
|
||||
public const FILENAME_RULE = 'Any-Latin; Latin-ASCII; [^A-Za-z0-9_\-] remove; Lower()';
|
||||
|
||||
public function __construct(InvoiceDocumentRepository $repository)
|
||||
private $repository;
|
||||
private $systemConfiguration;
|
||||
private $extensions = [];
|
||||
|
||||
public function __construct(InvoiceDocumentRepository $repository, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->systemConfiguration = $systemConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,22 +41,32 @@ class InvoiceDocumentUploadForm extends AbstractType
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->extensions = self::EXTENSIONS_NO_TWIG;
|
||||
$extensions = 'DOCX, ODS, XLSX';
|
||||
$mimetypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.oasis.opendocument.spreadsheet',
|
||||
];
|
||||
|
||||
if ((bool) $this->systemConfiguration->find('invoice.upload_twig') === true) {
|
||||
$this->extensions = self::EXTENSIONS;
|
||||
$extensions = 'DOCX, ODS, XLSX, TWIG (PDF & HTML)';
|
||||
$mimetypes = array_merge($mimetypes, [
|
||||
'application/octet-stream', // needed for twig templates
|
||||
'text/html', // needed for twig templates
|
||||
'text/plain', // needed for twig templates
|
||||
]);
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('document', FileType::class, [
|
||||
'label' => 'label.invoice_renderer',
|
||||
'translation_domain' => 'invoice-renderer',
|
||||
'help' => 'help.upload',
|
||||
'help_translation_parameters' => ['%extensions%' => $extensions],
|
||||
'mapped' => false,
|
||||
'required' => true,
|
||||
'attr' => [
|
||||
'accept' => implode(',', $mimetypes)
|
||||
],
|
||||
'constraints' => [
|
||||
new File([
|
||||
'mimeTypes' => $mimetypes,
|
||||
@@ -77,6 +95,48 @@ class InvoiceDocumentUploadForm extends AbstractType
|
||||
->setTranslationDomain('validators')
|
||||
->setCode('kimai-invoice-document-upload-01')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$extension = null;
|
||||
$nameWithoutExtension = null;
|
||||
|
||||
foreach ($this->extensions as $ext) {
|
||||
$len = \strlen($ext);
|
||||
if (substr_compare($name, $ext, -$len) === 0) {
|
||||
$extension = $ext;
|
||||
$nameWithoutExtension = str_replace($ext, '', $name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($extension === null) {
|
||||
$context->buildViolation('This invoice document cannot be used, allowed file extensions are: %extensions%')
|
||||
->setParameters(['%extensions%' => implode(', ', $this->extensions)])
|
||||
->setTranslationDomain('validators')
|
||||
->setCode('kimai-invoice-document-upload-02')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$safeFilename = transliterator_transliterate(self::FILENAME_RULE, $nameWithoutExtension);
|
||||
|
||||
if ($safeFilename !== $nameWithoutExtension) {
|
||||
$context->buildViolation('This invoice document cannot be used, filename may only contain the following ascii character: %character%')
|
||||
->setParameters(['%character%' => 'A-Z a-z 0-9 _ -'])
|
||||
->setTranslationDomain('validators')
|
||||
->setCode('kimai-invoice-document-upload-03')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (mb_strlen($nameWithoutExtension) > 20) {
|
||||
$context->buildViolation('This invoice document cannot be used, allowed filename length without extension is %character% character.')
|
||||
->setParameters(['%character%' => 20])
|
||||
->setTranslationDomain('validators')
|
||||
->setCode('kimai-invoice-document-upload-04')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\Type\DescriptionType;
|
||||
use App\Form\Type\MetaFieldsCollectionType;
|
||||
use App\Form\Type\TagsInputType;
|
||||
use App\Form\Type\UserType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -29,6 +30,7 @@ class TimesheetPreCreateForm extends AbstractType
|
||||
$this->addActivity($builder, null, null, ['required' => false]);
|
||||
$builder->add('description', DescriptionType::class, ['required' => false]);
|
||||
$builder->add('tags', TagsInputType::class, ['required' => false]);
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class, ['required' => false]);
|
||||
}
|
||||
|
||||
@@ -217,11 +217,17 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
break;
|
||||
|
||||
case 'cname':
|
||||
$partialResult = $this->model->getCustomer() !== null ? $this->model->getCustomer()->getName() : '';
|
||||
$partialResult = $this->model->getCustomer() !== null ? $this->model->getCustomer()->getName() : null;
|
||||
if ($partialResult === null) {
|
||||
throw new \InvalidArgumentException('Customer has no name, replacer {cname} failed evaluation.');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cnumber':
|
||||
$partialResult = $this->model->getCustomer() !== null ? $this->model->getCustomer()->getNumber() : '';
|
||||
$partialResult = $this->model->getCustomer() !== null ? $this->model->getCustomer()->getNumber() : null;
|
||||
if ($partialResult === null) {
|
||||
throw new \InvalidArgumentException('Customer has no number, replacer {cnumber} failed evaluation.');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Export\Base\DispositionInlineTrait;
|
||||
use App\Export\ExportContext;
|
||||
use App\Invoice\InvoiceFilename;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Utils\FileHelper;
|
||||
use App\Utils\HtmlToPdfConverter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
@@ -61,6 +62,8 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin
|
||||
|
||||
$response = new Response($content);
|
||||
|
||||
$filename = FileHelper::convertToAsciiFilename($filename);
|
||||
|
||||
$disposition = $response->headers->makeDisposition($this->getDisposition(), $filename . '.pdf');
|
||||
|
||||
$response->headers->set('Content-Type', 'application/pdf');
|
||||
|
||||
@@ -161,7 +161,7 @@ final class InvoiceDocumentRepository
|
||||
continue;
|
||||
}
|
||||
|
||||
$finder = Finder::create()->ignoreDotFiles(true)->files()->in($searchDir)->name('*.*');
|
||||
$finder = Finder::create()->ignoreDotFiles(true)->files()->in($searchDir)->depth(0)->name('*.*');
|
||||
foreach ($finder->getIterator() as $file) {
|
||||
$doc = new InvoiceDocument($file);
|
||||
// the first found invoice document wins
|
||||
|
||||
@@ -81,8 +81,20 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
|
||||
// when changing the date via the calendar and/or the API, the duration will not be reset by the
|
||||
// duration calculator (which runs after validation!) so we manually reset the duration before
|
||||
$timesheet->setDuration(null);
|
||||
$duration = $timesheet->getDuration();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Old solution (buggy) - duration MAY NOT BE RESET!
|
||||
// this will cause the timesheet to be deleted in the Weekly-QuickEntry-Flow
|
||||
// ------------------------------------------------------------------------
|
||||
// $timesheet->setDuration(null);
|
||||
// $duration = $timesheet->getDuration();
|
||||
|
||||
// another possible solution is cloning the timesheet
|
||||
// $timesheet = clone $timesheet;
|
||||
// $timesheet->setDuration(null);
|
||||
// $duration = $timesheet->getDuration();
|
||||
|
||||
$duration = $timesheet->getCalculatedDuration();
|
||||
|
||||
$timeRate = $this->rateService->calculate($timesheet);
|
||||
$rate = $timeRate->getRate();
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_CUSTOMER_DESCRIPTION') %}
|
||||
title = apiItem.project.customer.name + description;
|
||||
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_PROJECT_CUSTOMER') %}
|
||||
title = apiItem.project.name + apiItem.project.customer.name;
|
||||
title = apiItem.project.name + '{{ constant('\\App\\Form\\Type\\CalendarTitlePatternType::SPACER') }}' + apiItem.project.customer.name;
|
||||
{% endif %}
|
||||
|
||||
if (title === '' || title === null) {
|
||||
@@ -180,6 +180,13 @@
|
||||
{
|
||||
const escaper = kimai.getPlugin('escape');
|
||||
|
||||
let tags = '';
|
||||
if (eventObj.tags !== null && eventObj.tags.length > 0) {
|
||||
for (let tag of eventObj.tags) {
|
||||
tags += '<span class="badge bg-green">' + escaper.escapeForHtml(tag) + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
return '<div class="calendar-entry">' +
|
||||
'<ul>' +
|
||||
'<li>' + '{{ 'label.customer'|trans }}: ' + escaper.escapeForHtml(eventObj.customer) + '</li>' +
|
||||
@@ -187,8 +194,7 @@
|
||||
'<li>' + '{{ 'label.activity'|trans }}: ' + escaper.escapeForHtml(eventObj.activity) + '</li>' +
|
||||
'</ul>' +
|
||||
(eventObj.description !== null || eventObj.tags.length > 0 ? '<hr>' : '') +
|
||||
(eventObj.description ? '<p>' + eventObj.description + '</p>' : '') +
|
||||
(eventObj.tags !== null && eventObj.tags.length > 0 ? '<span class="badge bg-green">' + eventObj.tags.join('</span> <span class="badge bg-green">') + '</span>' : '') +
|
||||
(eventObj.description ? '<p>' + escaper.escapeForHtml(eventObj.description) + '</p>' : '') + tags +
|
||||
'</div>'
|
||||
;
|
||||
}
|
||||
|
||||
@@ -773,6 +773,36 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
|
||||
}
|
||||
|
||||
public function testGetRecentActionForUser(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
$start = new \DateTime('-10 days');
|
||||
|
||||
$user = $this->getUserByRole(User::ROLE_ADMIN);
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setFixedRate(true);
|
||||
$fixture->setHourlyRate(true);
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setUser($user);
|
||||
$fixture->setStartDate($start);
|
||||
$this->importFixture($fixture);
|
||||
|
||||
$query = [
|
||||
'user' => $user->getId(),
|
||||
'size' => 2,
|
||||
'begin' => $start->format(self::DATE_FORMAT_HTML5),
|
||||
];
|
||||
|
||||
$this->assertAccessIsGranted($client, '/api/timesheets/recent', 'GET', $query);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals(1, \count($result));
|
||||
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
|
||||
}
|
||||
|
||||
public function testActiveAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
|
||||
@@ -325,6 +325,7 @@ class ConfigurationTest extends TestCase
|
||||
],
|
||||
'simple_form' => false,
|
||||
'number_format' => '{Y}/{cy,3}',
|
||||
'upload_twig' => true,
|
||||
],
|
||||
'export' => [
|
||||
'documents' => [
|
||||
|
||||
@@ -31,7 +31,6 @@ class TimesheetExportRepositoryTest extends TestCase
|
||||
|
||||
$sut = new TimesheetExportRepository($repository);
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
$sut->setExported([new Timesheet(), null, new \stdClass(), new Timesheet(), new Activity()]);
|
||||
// test else for empty array
|
||||
/* @phpstan-ignore-next-line */
|
||||
|
||||
@@ -166,6 +166,38 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
||||
$this->assertEquals('default', $sut->getId());
|
||||
}
|
||||
|
||||
public function getMissingFieldTestData()
|
||||
{
|
||||
return [
|
||||
['{Y}/{cnumber}_{ccy,3}', 'Customer has no number, replacer {cnumber} failed evaluation.'],
|
||||
['{Y}/{cname}_{ccy,3}', 'Customer has no name, replacer {cname} failed evaluation.'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getMissingFieldTestData
|
||||
*/
|
||||
public function testCustomerHasMissingField(string $format, string $message)
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage($message);
|
||||
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('getId')->willReturn(13);
|
||||
$user->method('getAccountNumber')->willReturn('0815');
|
||||
|
||||
$customer = new Customer();
|
||||
|
||||
$sut = $this->getSut($format);
|
||||
$model = (new InvoiceModelFactoryFactory($this))->create()->createModel(new DebugFormatter());
|
||||
$model->setInvoiceDate(new \DateTime());
|
||||
$model->setCustomer($customer);
|
||||
$model->setUser($user);
|
||||
$sut->setModel($model);
|
||||
|
||||
$sut->getInvoiceNumber();
|
||||
}
|
||||
|
||||
public function getInvalidTestData()
|
||||
{
|
||||
$invoiceDate = new \DateTime();
|
||||
|
||||
@@ -31,7 +31,6 @@ class TimesheetInvoiceItemRepositoryTest extends TestCase
|
||||
|
||||
$sut = new TimesheetInvoiceItemRepository($repository);
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
$sut->setExported([new Timesheet(), null, new \stdClass(), new Timesheet(), new Activity()]);
|
||||
// test else for empty array
|
||||
/* @phpstan-ignore-next-line */
|
||||
|
||||
@@ -412,6 +412,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase
|
||||
$timesheet->method('getBegin')->willReturn($begin);
|
||||
$timesheet->method('getEnd')->willReturn($end);
|
||||
$timesheet->method('getDuration')->willReturn($end->getTimestamp() - $begin->getTimestamp());
|
||||
$timesheet->method('getCalculatedDuration')->willReturn($end->getTimestamp() - $begin->getTimestamp());
|
||||
$timesheet->method('getUser')->willReturn(new User());
|
||||
$timesheet->method('getProject')->willReturn($project);
|
||||
$timesheet->method('getActivity')->willReturn($activity);
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">انتباه: سيتم الكتابة فوق الملفات الموجودة. أنواع الملفات المسموح بها هي: DOCX و ODS و XLSX.</target>
|
||||
<target state="translated">انتباه: سيتم الكتابة فوق الملفات الموجودة. أنواع الملفات المسموح بها هي: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Pozor: existující soubory budou přepsány. Povolené typy jsou: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Pozor: existující soubory budou přepsány. Povolené typy jsou: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="needs-translation">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: DOCX, ODS, XLSX</target>
|
||||
<target state="needs-translation">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: %extensions%</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Προσοχή: τα υπάρχοντα αρχεία θα αντικατασταθούν. Επιτρεπόμενοι τύποι αρχείων είναι: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Προσοχή: τα υπάρχοντα αρχεία θα αντικατασταθούν. Επιτρεπόμενοι τύποι αρχείων είναι: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Attention: existing files will be overwritten. Allowed file types are: DOCX, ODS, XLSX.</target>
|
||||
<target>Attention: existing files will be overwritten. Allowed file types are: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Atentu: ekzistantaj dosieroj estos anstataŭigitaj. Permesitaj tipoj de dosieroj estas: DOCX, ODS, XLSX.</target>
|
||||
<target>Atentu: ekzistantaj dosieroj estos anstataŭigitaj. Permesitaj tipoj de dosieroj estas: %extensions%.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Atención: Los archivos existentes serán sobrescritos. Los tipos de archivo permitidos son: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Atención: Los archivos existentes serán sobrescritos. Los tipos de archivo permitidos son: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">توجه: فایل های موجود رونویسی خواهند شد. فایل های مجاز عبارتند از: DOCX، ODS، XLSX.</target>
|
||||
<target state="translated">توجه: فایل های موجود رونویسی خواهند شد. فایل های مجاز عبارتند از: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Huomioitavaa: olemassa olevat tiedostot korvataan. Sallitut tiedosto tyypit on: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Huomioitavaa: olemassa olevat tiedostot korvataan. Sallitut tiedosto tyypit on: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="needs-translation">Attention: existing files will be overwritten. Allowed file types are: DOCX, ODS, XLSX.</target>
|
||||
<target state="needs-translation">Attention: existing files will be overwritten. Allowed file types are: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Attention : les fichiers existants seront écrasés. Les types de fichiers autorisés sont : DOCX, ODS, XLSX.</target>
|
||||
<target>Attention : les fichiers existants seront écrasés. Les types de fichiers autorisés sont : %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>שים לב: קבצים קיימים ידרסו וימחקו. סוגי קבצים מותרים: DOCX, ODS, XLSX.</target>
|
||||
<target>שים לב: קבצים קיימים ידרסו וימחקו. סוגי קבצים מותרים: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sjwwrr8" resname="Xlsx">
|
||||
<source>Microsoft Excel (XLSX)</source>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Oprez: postojeće datoteke će se prepisati. Dopuštene vrste datoteka su: DOCX, ODS, XLSX.</target>
|
||||
<target>Oprez: postojeće datoteke će se prepisati. Dopuštene vrste datoteka su: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Figyelem: a meglévő fájlok felülíródnak. A megengedett fájltípusok: DOCX, ODS, XLSX.</target>
|
||||
<target>Figyelem: a meglévő fájlok felülíródnak. A megengedett fájltípusok: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>주의: 기존 파일을 덮어씁니다. 허용되는 파일 형식은 DOCX, ODS, XLSX입니다.</target>
|
||||
<target>주의: 기존 파일을 덮어씁니다. 허용되는 파일 형식은 %extensions%입니다.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="nA.KOyM" resname="Html">
|
||||
<source>Html</source>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Obs: Eksisterende filer vil bli overskrevet. Filtypene DOCX, ODS og XLSX støttes.</target>
|
||||
<target>Obs: Eksisterende filer vil bli overskrevet. Filtypene %extensions% støttes.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Attentie: Bestaande bestanden zullen overschreven worden. Toegestaande bestandstypes zijn: DOCX, ODS, XLSX.</target>
|
||||
<target>Attentie: Bestaande bestanden zullen overschreven worden. Toegestaande bestandstypes zijn: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Uwaga: istniejące pliki zostaną nadpisane. Dozwolone typu plików to: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Uwaga: istniejące pliki zostaną nadpisane. Dozwolone typu plików to: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Atenção: os ficheiros já existentes serão substituídos. Os tipos de ficheiros permitidos são: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Atenção: os ficheiros já existentes serão substituídos. Os tipos de ficheiros permitidos são: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Atenção: os arquivos já existentes serão substituídos. Os tipos dos arquivos permitidos são: DOCX, ODS, XLSX.</target>
|
||||
<target>Atenção: os arquivos já existentes serão substituídos. Os tipos dos arquivos permitidos são: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="nA.KOyM" resname="Html">
|
||||
<source>Html</source>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Atenție: fișierele existente vor fi suprascrise. Tipuri de fișiere permise: DOCX, ODS, XLSX.</target>
|
||||
<target>Atenție: fișierele existente vor fi suprascrise. Tipuri de fișiere permise: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Внимание: существующие файлы будут перезаписаны. Допустимые типы файлов: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Внимание: существующие файлы будут перезаписаны. Допустимые типы файлов: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Obs: redan exsiterande filer kommer att skrivas över. Tillåtna fil typer är: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Obs: redan exsiterande filer kommer att skrivas över. Tillåtna fil typer är: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="N6juwc4" resname="default">
|
||||
<source>default</source>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Dikkat: mevcut dosyaların üzerine yazılacaktır. İzin verilen dosya türleri: DOCX, ODS, XLSX.</target>
|
||||
<target>Dikkat: mevcut dosyaların üzerine yazılacaktır. İzin verilen dosya türleri: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="nA.KOyM" resname="Html">
|
||||
<source>Html</source>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">Увага: існуючі файли будуть перезаписані. Дозволені типи файлів: DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">Увага: існуючі файли будуть перезаписані. Дозволені типи файлів: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Chú ý: các tệp hiện có sẽ bị ghi đè. Các loại tệp được phép là: DOCX, ODS, XLSX.</target>
|
||||
<target>Chú ý: các tệp hiện có sẽ bị ghi đè. Các loại tệp được phép là: %extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7mEUv6C" resname="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target state="translated">注意:现有文件将被覆盖。允许的文件类型是:DOCX, ODS, XLSX.</target>
|
||||
<target state="translated">注意:现有文件将被覆盖。允许的文件类型是:%extensions%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="E5BwZHT" resname="programmatic">
|
||||
<source>programmatic</source>
|
||||
|
||||
Reference in New Issue
Block a user