mirror of
https://github.com/kevinpapst/kimai2.git
synced 2025-04-07 22:45:50 +00:00
Merge branch 'master' into PdfExportChanges
This commit is contained in:
commit
15e69c419a
85 changed files with 1689 additions and 1163 deletions
.github/workflows
.github_changelog_generatorSECURITY.mdcomposer.jsoncomposer.lockconfig/packages/prod
phpstan.neonsrc
API
Command
Configuration
Constants.phpController
DependencyInjection
Entity
Activity.phpBudgetTrait.phpColorTrait.phpConfiguration.phpCustomer.phpInvoice.phpInvoiceTemplate.phpMetaTableTypeTrait.phpProject.phpRate.phpRole.phpRolePermission.phpTag.phpTeam.phpTeamMember.phpTimesheet.phpUser.phpUserPreference.php
Event
Export
Invoice
Plugin
Repository
Twig
Utils
Voter
templates
export
plugin
reporting
tests
API
Command
ActivateUserCommandTest.phpChangePasswordCommandTest.phpCreateUserCommandTest.phpDeactivateUserCommandTest.phpDemoteUserCommandTest.phpImportCustomerCommandTest.phpImportProjectCommandTest.phpImportTimesheetCommandTest.phpInstallCommandTest.phpInvoiceCreateCommandTest.phpKimaiImporterCommandTest.phpPromoteUserCommandTest.phpReloadCommandTest.phpVersionCommandTest.php
Entity
Event
Repository
phpstan-doctrine.phpphpstan.neontranslations
3
.github/workflows/testing.yaml
vendored
3
.github/workflows/testing.yaml
vendored
|
@ -51,6 +51,9 @@ jobs:
|
|||
- name: Validate Composer
|
||||
run: composer validate --strict
|
||||
|
||||
- name: Warmup cache for PHPStan
|
||||
run: APP_ENV=dev bin/console kimai:reload -n
|
||||
|
||||
- name: Check codestyles
|
||||
run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none --format=checkstyle | cs2pr
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
unreleased=true
|
||||
future-release=1.16
|
||||
future-release=1.17
|
||||
exclude-labels=duplicate,support,question,invalid,wontfix,release,waiting for feedback,documentation
|
||||
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt,documentation
|
||||
issues-wo-labels=false
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
As announced in the [README](README.md) I only support the latest available release and master.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
|---------| ------------------ |
|
||||
| master | :white_check_mark: |
|
||||
| 1.16 | :white_check_mark: |
|
||||
| < 1.16 | :x: |
|
||||
| 1.17 | :white_check_mark: |
|
||||
| < 1.17 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
"dama/doctrine-test-bundle": "^6.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"fakerphp/faker": "^1.15",
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"friendsofphp/php-cs-fixer": "3.2.*",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpstan/phpstan-doctrine": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
|
|
1744
composer.lock
generated
1744
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -9,6 +9,7 @@ monolog:
|
|||
- ^/
|
||||
nested:
|
||||
type: stream
|
||||
level: info
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
|
||||
console:
|
||||
|
|
15
phpstan.neon
15
phpstan.neon
|
@ -1,8 +1,9 @@
|
|||
includes:
|
||||
- vendor/phpstan/phpstan-symfony/extension.neon
|
||||
- vendor/phpstan/phpstan-doctrine/extension.neon
|
||||
- vendor/phpstan/phpstan-symfony/rules.neon
|
||||
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
|
||||
- %rootDir%/../phpstan-symfony/extension.neon
|
||||
- %rootDir%/../phpstan-symfony/rules.neon
|
||||
- %rootDir%/../phpstan-doctrine/extension.neon
|
||||
- %rootDir%/../phpstan-doctrine/rules.neon
|
||||
- %rootDir%/../phpstan/conf/bleedingEdge.neon
|
||||
|
||||
parameters:
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
|
@ -11,4 +12,8 @@ parameters:
|
|||
excludePaths:
|
||||
- %rootDir%/../../../src/Ldap/LdapDriver.php
|
||||
treatPhpDocTypesAsCertain: false
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
doctrine:
|
||||
objectManagerLoader: %rootDir%/../../../tests/phpstan-doctrine.php
|
||||
symfony:
|
||||
containerXmlPath: %rootDir%/../../../var/cache/dev/srcApp_KernelDevDebugContainer.xml
|
||||
|
|
|
@ -15,7 +15,6 @@ use App\Entity\Activity;
|
|||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\TeamMember;
|
||||
use App\Entity\User;
|
||||
use App\Form\API\TeamApiEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
|
@ -201,7 +200,7 @@ final class TeamController extends BaseApiController
|
|||
* Update an existing team
|
||||
*
|
||||
* @SWG\Patch(
|
||||
* description="Update an existing team, you can pass all or just a subset of all attributes (passing users will replace all existing ones)",
|
||||
* description="Update an existing team, you can pass all or just a subset of all attributes (passing members will replace all existing ones)",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the updated team",
|
||||
|
@ -235,11 +234,12 @@ final class TeamController extends BaseApiController
|
|||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// cache the current memberlist
|
||||
/** @var TeamMember[] $originalMembers */
|
||||
$originalMembers = [];
|
||||
foreach ($team->getMembers() as $member) {
|
||||
$originalMembers[] = $member;
|
||||
if ($request->request->has('members')) {
|
||||
foreach ($team->getMembers() as $member) {
|
||||
$team->removeMember($member);
|
||||
$this->repository->removeTeamMember($member);
|
||||
}
|
||||
$this->repository->saveTeam($team);
|
||||
}
|
||||
|
||||
$form = $this->createForm(TeamApiEditForm::class, $team);
|
||||
|
@ -254,14 +254,6 @@ final class TeamController extends BaseApiController
|
|||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
// and now remove the ones, which are not in the list any longer
|
||||
foreach ($originalMembers as $member) {
|
||||
if (!$team->hasMember($member)) {
|
||||
$member->getUser()->removeMembership($member);
|
||||
$this->repository->removeTeamMember($member);
|
||||
}
|
||||
}
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
|
|
|
@ -100,7 +100,7 @@ final class CreateUserCommand extends Command
|
|||
*/
|
||||
protected function askForPassword(InputInterface $input, OutputInterface $output): string
|
||||
{
|
||||
/* @var QuestionHelper $helper */
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
$passwordQuestion = new Question('Please enter the password: ');
|
||||
|
|
|
@ -105,7 +105,7 @@ trait StringAccessibleConfigTrait
|
|||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
* @return string|int|bool|float|null|array
|
||||
*/
|
||||
public function find(string $key)
|
||||
{
|
||||
|
|
|
@ -17,11 +17,11 @@ class Constants
|
|||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '1.16.10';
|
||||
public const VERSION = '1.17.1';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 11610;
|
||||
public const VERSION_ID = 11701;
|
||||
/**
|
||||
* The current release status, either "stable" or "dev"
|
||||
*/
|
||||
|
|
|
@ -295,7 +295,7 @@ final class InvoiceController extends AbstractController
|
|||
$csrfTokenManager->refreshToken('invoice.status');
|
||||
|
||||
try {
|
||||
$this->service->deleteInvoice($invoice);
|
||||
$this->service->deleteInvoice($invoice, $this->dispatcher);
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\TeamMember;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
|
@ -19,7 +18,9 @@ use App\Form\UserPasswordType;
|
|||
use App\Form\UserPreferencesForm;
|
||||
use App\Form\UserRolesType;
|
||||
use App\Form\UserTeamsType;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use App\User\UserService;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
@ -78,15 +79,13 @@ final class ProfileController extends AbstractController
|
|||
* @Route(path="/{username}/edit", name="user_profile_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', profile)")
|
||||
*/
|
||||
public function editAction(User $profile, Request $request): Response
|
||||
public function editAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$form = $this->createEditForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
$userRepository->saveUser($profile);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
|
@ -152,7 +151,7 @@ final class ProfileController extends AbstractController
|
|||
* @Route(path="/{username}/roles", name="user_profile_roles", methods={"GET", "POST"})
|
||||
* @Security("is_granted('roles', profile)")
|
||||
*/
|
||||
public function rolesAction(User $profile, Request $request): Response
|
||||
public function rolesAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$isSuperAdmin = $profile->isSuperAdmin();
|
||||
|
||||
|
@ -166,9 +165,7 @@ final class ProfileController extends AbstractController
|
|||
$profile->setSuperAdmin(true);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
$userRepository->saveUser($profile);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
|
@ -186,7 +183,7 @@ final class ProfileController extends AbstractController
|
|||
* @Route(path="/{username}/teams", name="user_profile_teams", methods={"GET", "POST"})
|
||||
* @Security("is_granted('teams', profile)")
|
||||
*/
|
||||
public function teamsAction(User $profile, Request $request, UserService $service): Response
|
||||
public function teamsAction(User $profile, Request $request, UserRepository $userRepository, TeamRepository $teamRepository): Response
|
||||
{
|
||||
$originalMembers = new ArrayCollection();
|
||||
foreach ($profile->getMemberships() as $member) {
|
||||
|
@ -197,18 +194,15 @@ final class ProfileController extends AbstractController
|
|||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
|
||||
/** @var TeamMember $member */
|
||||
foreach ($originalMembers as $member) {
|
||||
if (!$profile->hasMembership($member)) {
|
||||
$member->getTeam()->removeMember($member);
|
||||
$entityManager->remove($profile);
|
||||
$member->setTeam(null);
|
||||
$member->setUser(null);
|
||||
$teamRepository->removeTeamMember($member);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
$userRepository->saveUser($profile);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
|
|
|
@ -100,6 +100,7 @@ class AppExtension extends Extension
|
|||
$container->setParameter('kimai.i18n_domains', $localTranslations);
|
||||
|
||||
// this should happen always at the end, so bundles do not mess with the base configuration
|
||||
/* @phpstan-ignore-next-line */
|
||||
if ($container->hasParameter('kimai.bundles.config')) {
|
||||
$bundleConfig = $container->getParameter('kimai.bundles.config');
|
||||
if (!\is_array($bundleConfig)) {
|
||||
|
|
|
@ -99,7 +99,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
|||
/**
|
||||
* Description of this activity
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity"})
|
||||
|
|
|
@ -51,7 +51,7 @@ trait BudgetTrait
|
|||
* - null = default / full time
|
||||
* - month = monthly budget
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity", "Project_Entity", "Customer_Entity"})
|
||||
|
|
|
@ -20,7 +20,7 @@ trait ColorTrait
|
|||
/**
|
||||
* The assigned color in HTML hex format, eg. #dd1d00
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
@ -32,9 +32,6 @@ trait ColorTrait
|
|||
*/
|
||||
private $color = null;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getColor(): ?string
|
||||
{
|
||||
if ($this->color === Constants::DEFAULT_COLOR) {
|
||||
|
@ -49,9 +46,6 @@ trait ColorTrait
|
|||
return null !== $this->color && $this->color !== Constants::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $color
|
||||
*/
|
||||
public function setColor(?string $color = null): void
|
||||
{
|
||||
$this->color = $color;
|
||||
|
|
|
@ -30,10 +30,8 @@ class Configuration
|
|||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -42,9 +40,8 @@ class Configuration
|
|||
* @Assert\Length(min=2, max=100, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="value", type="string", length=1024, nullable=true)
|
||||
* @Assert\Length(max=1024, allowEmptyString=true)
|
||||
|
|
|
@ -64,7 +64,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
@ -76,7 +76,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $number;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -99,7 +99,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $visible = true;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -111,7 +111,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $company;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -123,7 +123,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $vatId;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -135,7 +135,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $contact;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -174,7 +174,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $currency = self::DEFAULT_CURRENCY;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -186,7 +186,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $phone;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -198,7 +198,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $fax;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -214,7 +214,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*
|
||||
* Limited via RFC to 254 chars
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -226,7 +226,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $email;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
|
|
@ -53,7 +53,6 @@ class Invoice implements EntityWithMetaFields
|
|||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -63,9 +62,8 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\NotNull()
|
||||
*/
|
||||
private $invoiceNumber;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
|
@ -75,25 +73,22 @@ class Invoice implements EntityWithMetaFields
|
|||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
/**
|
||||
* @var Customer|null
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
|
||||
/**
|
||||
* @var User|null
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
|
@ -103,14 +98,12 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\NotNull()
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=64, nullable=false)
|
||||
*/
|
||||
private $timezone;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
|
@ -120,7 +113,6 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\NotNull()
|
||||
*/
|
||||
private $total = 0.00;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
|
@ -130,7 +122,6 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\NotNull()
|
||||
*/
|
||||
private $tax = 0.00;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -141,7 +132,6 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\Length(max=3)
|
||||
*/
|
||||
private $currency;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
|
@ -152,7 +142,6 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\Range(min = 0, max = 999)
|
||||
*/
|
||||
private $dueDays = 30;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
|
@ -163,7 +152,6 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\Range(min = 0.0, max = 99.99)
|
||||
*/
|
||||
private $vat = 0.00;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -173,7 +161,6 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\NotNull()
|
||||
*/
|
||||
private $status = self::STATUS_NEW;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -184,19 +171,16 @@ class Invoice implements EntityWithMetaFields
|
|||
* @Assert\Length(min=1, max=150, allowEmptyString=false)
|
||||
*/
|
||||
private $invoiceFilename;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $localized = false;
|
||||
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @ORM\Column(name="payment_date", type="date", nullable=true)
|
||||
*/
|
||||
private $paymentDate;
|
||||
|
||||
/**
|
||||
* Meta fields
|
||||
*
|
||||
|
|
|
@ -32,7 +32,6 @@ class InvoiceTemplate
|
|||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -41,7 +40,6 @@ class InvoiceTemplate
|
|||
* @Assert\Length(min=1, max=60, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -49,7 +47,6 @@ class InvoiceTemplate
|
|||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -57,29 +54,25 @@ class InvoiceTemplate
|
|||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="vat_id", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $vatId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="address", type="text", nullable=true)
|
||||
*/
|
||||
private $address;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="contact", type="text", nullable=true)
|
||||
*/
|
||||
private $contact;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
|
@ -87,7 +80,6 @@ class InvoiceTemplate
|
|||
* @Assert\Range(min = 0, max = 999)
|
||||
*/
|
||||
private $dueDays = 30;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
|
@ -95,7 +87,6 @@ class InvoiceTemplate
|
|||
* @Assert\Range(min = 0.0, max = 99.99)
|
||||
*/
|
||||
private $vat = 0.00;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -112,7 +103,6 @@ class InvoiceTemplate
|
|||
* @Assert\Length(max=20)
|
||||
*/
|
||||
private $numberGenerator = 'default';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
|
@ -121,21 +111,18 @@ class InvoiceTemplate
|
|||
* @Assert\Length(max=20)
|
||||
*/
|
||||
private $renderer = 'default';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="payment_terms", type="text", nullable=true)
|
||||
*/
|
||||
private $paymentTerms;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="payment_details", type="text", nullable=true)
|
||||
*/
|
||||
private $paymentDetails;
|
||||
|
||||
/**
|
||||
* Used when rendering HTML templates.
|
||||
*
|
||||
|
@ -145,11 +132,10 @@ class InvoiceTemplate
|
|||
* @Assert\NotNull()
|
||||
*/
|
||||
private $decimalDuration = false;
|
||||
|
||||
/**
|
||||
* Used for translations and locale dependent number and date formats.
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="language", type="string", length=6, nullable=true)
|
||||
*/
|
||||
|
|
|
@ -45,7 +45,7 @@ trait MetaTableTypeTrait
|
|||
/**
|
||||
* Value of the meta (custom) field
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
|
|
@ -106,7 +106,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||
/**
|
||||
* Project order number
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
|
@ -118,7 +118,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $orderNumber;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
|
@ -133,7 +133,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $orderDate;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project"})
|
||||
|
@ -148,7 +148,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $start;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project"})
|
||||
|
@ -163,7 +163,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $end;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
* @internal used for storing the timezone for "order", "start" and "end" date
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=64, nullable=true)
|
||||
|
@ -175,7 +175,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||
*/
|
||||
private $localized = false;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
|
|
|
@ -28,7 +28,7 @@ trait Rate
|
|||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var User
|
||||
* @var User|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
|
|
@ -30,7 +30,6 @@ class Role
|
|||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
|
|
|
@ -30,7 +30,6 @@ class RolePermission
|
|||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
|
|
|
@ -39,7 +39,6 @@ class Tag
|
|||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
|
|
|
@ -59,7 +59,7 @@ class Team
|
|||
/**
|
||||
* All team member (including team leads)
|
||||
*
|
||||
* @var TeamMember[]|Collection<TeamMember>
|
||||
* @var Collection<TeamMember>
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Team_Entity"})
|
||||
|
@ -135,22 +135,11 @@ class Team
|
|||
}
|
||||
|
||||
/**
|
||||
* Indexed by ID to use it within collection type forms.
|
||||
*
|
||||
* @return TeamMember[]
|
||||
* @return Collection<TeamMember>
|
||||
*/
|
||||
public function getMembers(): iterable
|
||||
public function getMembers(): Collection
|
||||
{
|
||||
$all = [];
|
||||
foreach ($this->members as $member) {
|
||||
if ($member->getId() === null) {
|
||||
$all[] = $member;
|
||||
} else {
|
||||
$all[$member->getId()] = $member;
|
||||
}
|
||||
}
|
||||
|
||||
return $all;
|
||||
return $this->members;
|
||||
}
|
||||
|
||||
public function addMember(TeamMember $member): void
|
||||
|
@ -167,12 +156,12 @@ class Team
|
|||
throw new \InvalidArgumentException('Cannot set foreign team membership');
|
||||
}
|
||||
|
||||
// when using the API an invalid user id does not trigger the validation first, but after calling this method :-(
|
||||
// when using the API an invalid User ID triggers the validation too late
|
||||
if ($member->getUser() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== ($existing = $this->findMember($member))) {
|
||||
if (null !== $this->findMemberByUser($member->getUser())) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -185,22 +174,11 @@ class Team
|
|||
return $this->members->contains($member);
|
||||
}
|
||||
|
||||
private function findMember(TeamMember $member): ?TeamMember
|
||||
{
|
||||
foreach ($this->members as $oldMember) {
|
||||
if ($oldMember->getUser() === $member->getUser() && $oldMember->getTeam() === $member->getTeam()) {
|
||||
return $oldMember;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function findMemberByUser(User $user): ?TeamMember
|
||||
{
|
||||
foreach ($this->members as $oldMember) {
|
||||
if ($oldMember->getUser() === $user) {
|
||||
return $oldMember;
|
||||
foreach ($this->members as $member) {
|
||||
if ($member->getUser() === $user) {
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,12 +187,14 @@ class Team
|
|||
|
||||
public function removeMember(TeamMember $member): void
|
||||
{
|
||||
if (null === ($existingMember = $this->findMember($member))) {
|
||||
if (!$this->members->contains($member)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->members->removeElement($existingMember);
|
||||
$existingMember->getUser()->removeMembership($existingMember);
|
||||
$this->members->removeElement($member);
|
||||
$member->getUser()->removeMembership($member);
|
||||
$member->setTeam(null);
|
||||
$member->setUser(null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,7 +32,6 @@ class TeamMember
|
|||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
|
@ -108,7 +107,7 @@ class TeamMember
|
|||
public function __clone()
|
||||
{
|
||||
if ($this->id !== null) {
|
||||
$id = null;
|
||||
$this->id = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
|||
*/
|
||||
private $begin;
|
||||
/**
|
||||
* @var DateTime
|
||||
* @var DateTime|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
@ -160,7 +160,7 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
|||
*/
|
||||
private $localized = false;
|
||||
/**
|
||||
* @var int
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
@ -202,7 +202,7 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
|||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
@ -240,7 +240,7 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
|||
*/
|
||||
private $fixedRate = null;
|
||||
/**
|
||||
* @var float
|
||||
* @var float|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Entity"})
|
||||
|
|
|
@ -100,7 +100,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
/**
|
||||
* The user alias will be displayed in the frontend instead of the username
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
@ -114,7 +114,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
/**
|
||||
* Registration date for the user
|
||||
*
|
||||
* @var DateTime
|
||||
* @var DateTime|null
|
||||
*
|
||||
* @Exporter\Expose(label="profile.registration_date", type="datetime")
|
||||
*
|
||||
|
@ -124,7 +124,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
/**
|
||||
* An additional title for the user, like the Job position or Department
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
|
@ -136,9 +136,9 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
*/
|
||||
private $title;
|
||||
/**
|
||||
* URL to the users avatar, will be auto-generated if empty
|
||||
* URL to the user avatar, will be auto-generated if empty
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
|
@ -150,7 +150,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
/**
|
||||
* API token (password) for this user
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="api_token", type="string", length=255, nullable=true)
|
||||
*/
|
||||
|
@ -167,7 +167,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
*
|
||||
* List of preferences for this user, required ones have dedicated fields/methods
|
||||
*
|
||||
* @var UserPreference[]|Collection
|
||||
* @var Collection<UserPreference>
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\UserPreference", mappedBy="user", cascade={"persist"})
|
||||
*/
|
||||
|
@ -175,7 +175,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
/**
|
||||
* List of all team memberships.
|
||||
*
|
||||
* @var TeamMember[]|ArrayCollection<TeamMember>
|
||||
* @var Collection<TeamMember>
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
|
@ -187,9 +187,9 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
*/
|
||||
private $memberships;
|
||||
/**
|
||||
* The type of authentication used by the user (eg. "kimai", "ldap", "saml")
|
||||
* The type of authentication used by the user (e.g. "kimai", "ldap", "saml")
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
* @internal for internal usage only
|
||||
*
|
||||
* @ORM\Column(name="auth", type="string", length=20, nullable=true)
|
||||
|
@ -579,12 +579,12 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
throw new \InvalidArgumentException('Cannot set foreign user membership');
|
||||
}
|
||||
|
||||
// when using the API an invalid user id does not trigger the validation first, but after calling this method :-(
|
||||
// when using the API an invalid Team ID triggers the validation too late
|
||||
if ($member->getTeam() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== ($existing = $this->findMember($member))) {
|
||||
if (null !== $this->findMemberByTeam($member->getTeam())) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -592,33 +592,35 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
$member->getTeam()->addMember($member);
|
||||
}
|
||||
|
||||
private function findMemberByTeam(Team $team): ?TeamMember
|
||||
{
|
||||
foreach ($this->memberships as $member) {
|
||||
if ($member->getTeam() === $team) {
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function removeMembership(TeamMember $member): void
|
||||
{
|
||||
if (null === ($member = $this->findMember($member))) {
|
||||
if (!$this->memberships->contains($member)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->memberships->removeElement($member);
|
||||
$member->getUser()->removeMembership($member);
|
||||
$member->getTeam()->removeMember($member);
|
||||
$member->setUser(null);
|
||||
$member->setTeam(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexed by ID to use it within collection type forms.
|
||||
*
|
||||
* @return TeamMember[]
|
||||
* @return Collection<TeamMember>
|
||||
*/
|
||||
public function getMemberships(): iterable
|
||||
public function getMemberships(): Collection
|
||||
{
|
||||
$all = [];
|
||||
foreach ($this->memberships as $member) {
|
||||
if ($member->getId() === null) {
|
||||
$all[] = $member;
|
||||
} else {
|
||||
$all[$member->getId()] = $member;
|
||||
}
|
||||
}
|
||||
|
||||
return $all;
|
||||
return $this->memberships;
|
||||
}
|
||||
|
||||
public function hasMembership(TeamMember $member): bool
|
||||
|
@ -626,17 +628,6 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
return $this->memberships->contains($member);
|
||||
}
|
||||
|
||||
private function findMember(TeamMember $member): ?TeamMember
|
||||
{
|
||||
foreach ($this->memberships as $oldMember) {
|
||||
if ($oldMember->getUser() === $member->getUser() && $oldMember->getTeam() === $member->getTeam()) {
|
||||
return $oldMember;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is member of any team.
|
||||
*
|
||||
|
@ -750,10 +741,8 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
|
||||
public function isTeamleadOf(Team $team): bool
|
||||
{
|
||||
foreach ($this->memberships as $membership) {
|
||||
if ($membership->getTeam() === $team) {
|
||||
return $membership->isTeamlead();
|
||||
}
|
||||
if (null !== ($member = $this->findMemberByTeam($team))) {
|
||||
return $member->isTeamlead();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -830,10 +819,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
|||
return $this->auth === null || $this->auth === self::AUTH_INTERNAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addRole($role)
|
||||
public function addRole(string $role)
|
||||
{
|
||||
$role = strtoupper($role);
|
||||
if ($role === static::DEFAULT_ROLE) {
|
||||
|
|
|
@ -49,7 +49,7 @@ class UserPreference
|
|||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="preferences")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
|
@ -65,7 +65,7 @@ class UserPreference
|
|||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
|
|
28
src/Event/InvoiceDeleteEvent.php
Normal file
28
src/Event/InvoiceDeleteEvent.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?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\Event;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class InvoiceDeleteEvent extends Event
|
||||
{
|
||||
private $invoice;
|
||||
|
||||
public function __construct(Invoice $invoice)
|
||||
{
|
||||
$this->invoice = $invoice;
|
||||
}
|
||||
|
||||
public function getInvoice(): Invoice
|
||||
{
|
||||
return $this->invoice;
|
||||
}
|
||||
}
|
|
@ -10,14 +10,14 @@
|
|||
namespace App\Export\Base;
|
||||
|
||||
use App\Activity\ActivityStatisticService;
|
||||
use App\Export\ExportItemInterface;
|
||||
use App\Invoice\InvoiceItemInterface;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
|
||||
trait RendererTrait
|
||||
{
|
||||
/**
|
||||
* @param ExportItemInterface[] $exportItems
|
||||
* @param InvoiceItemInterface[] $exportItems
|
||||
* @return array
|
||||
*/
|
||||
protected function calculateSummary(array $exportItems)
|
||||
|
@ -130,7 +130,7 @@ trait RendererTrait
|
|||
}
|
||||
|
||||
/**
|
||||
* @param ExportItemInterface[] $exportItems
|
||||
* @param InvoiceItemInterface[] $exportItems
|
||||
* @param TimesheetQuery $query
|
||||
* @param ProjectStatisticService $projectStatisticService
|
||||
* @return array
|
||||
|
@ -209,7 +209,7 @@ trait RendererTrait
|
|||
}
|
||||
|
||||
/**
|
||||
* @param ExportItemInterface[] $exportItems
|
||||
* @param InvoiceItemInterface[] $exportItems
|
||||
* @param TimesheetQuery $query
|
||||
* @param ActivityStatisticService $activityStatisticService
|
||||
* @return array
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
namespace App\Export;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Utils\FileHelper;
|
||||
|
||||
|
@ -19,40 +21,35 @@ final class ExportFilename
|
|||
* @var string
|
||||
*/
|
||||
private $filename;
|
||||
/**
|
||||
* @var Customer|null
|
||||
*/
|
||||
private $customer;
|
||||
/**
|
||||
* @var Project|null
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(TimesheetQuery $query)
|
||||
{
|
||||
$filename = date('Ymd');
|
||||
$hasName = false;
|
||||
|
||||
$customers = $query->getCustomers();
|
||||
if (\count($customers) === 1) {
|
||||
$filename .= '-' . $this->convert($this->getCustomerName($customers[0]));
|
||||
$hasName = true;
|
||||
$this->customer = $customers[0];
|
||||
}
|
||||
|
||||
$projects = $query->getProjects();
|
||||
if (\count($projects) === 1) {
|
||||
if (!$hasName) {
|
||||
$filename .= '-' . $this->convert($this->getCustomerName($projects[0]->getCustomer()));
|
||||
}
|
||||
$filename .= '-' . $this->convert($projects[0]->getName());
|
||||
$hasName = true;
|
||||
$this->project = $projects[0];
|
||||
}
|
||||
|
||||
$users = $query->getUsers();
|
||||
if (\count($users) === 1) {
|
||||
$filename .= '-' . $this->convert($users[0]->getDisplayName());
|
||||
$hasName = true;
|
||||
$this->user = $users[0];
|
||||
}
|
||||
|
||||
if (!$hasName) {
|
||||
$filename .= '-kimai-export';
|
||||
}
|
||||
|
||||
$filename = str_replace(['/', '\\'], '-', $filename);
|
||||
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
private function getCustomerName(Customer $customer): string
|
||||
|
@ -72,6 +69,37 @@ final class ExportFilename
|
|||
|
||||
public function getFilename()
|
||||
{
|
||||
if ($this->filename === null) {
|
||||
$filename = date('Ymd');
|
||||
$hasName = false;
|
||||
|
||||
if ($this->customer !== null) {
|
||||
$filename .= '-' . $this->convert($this->getCustomerName($this->customer));
|
||||
$hasName = true;
|
||||
}
|
||||
|
||||
if ($this->project !== null) {
|
||||
if (!$hasName) {
|
||||
$filename .= '-' . $this->convert($this->getCustomerName($this->project->getCustomer()));
|
||||
}
|
||||
$filename .= '-' . $this->convert($this->project->getName());
|
||||
$hasName = true;
|
||||
}
|
||||
|
||||
if ($this->user !== null) {
|
||||
$filename .= '-' . $this->convert($this->user->getDisplayName());
|
||||
$hasName = true;
|
||||
}
|
||||
|
||||
if (!$hasName) {
|
||||
$filename .= '-kimai-export';
|
||||
}
|
||||
|
||||
$filename = str_replace(['/', '\\'], '-', $filename);
|
||||
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,7 @@ namespace App\Invoice\Renderer;
|
|||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use Symfony\Bridge\Twig\Extension\TranslationExtension;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use App\Twig\TwigRendererTrait;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
|
@ -22,6 +20,8 @@ use Twig\Environment;
|
|||
*/
|
||||
abstract class AbstractTwigRenderer implements RendererInterface
|
||||
{
|
||||
use TwigRendererTrait;
|
||||
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
|
@ -34,39 +34,16 @@ abstract class AbstractTwigRenderer implements RendererInterface
|
|||
|
||||
protected function renderTwigTemplate(InvoiceDocument $document, InvoiceModel $model): string
|
||||
{
|
||||
$previousLocale = $this->changeTwigLocale($this->twig, $model->getTemplate()->getLanguage());
|
||||
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
$language = $model->getTemplate()->getLanguage();
|
||||
$formatLocale = $model->getFormatter()->getLocale();
|
||||
$template = '@invoice/' . basename($document->getFilename());
|
||||
$options = [
|
||||
// model should not be used in the future, but we can likely not remove it
|
||||
'model' => $model,
|
||||
// new since 1.16.7 - templates should only use the pre-generated values
|
||||
'invoice' => $model->toArray(),
|
||||
]);
|
||||
];
|
||||
|
||||
$this->changeTwigLocale($this->twig, $previousLocale);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function changeTwigLocale(Environment $twig, ?string $locale = null): ?string
|
||||
{
|
||||
// for invoices that don't have a language configured (using request locale)
|
||||
if (null === $locale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var TranslationExtension $extension */
|
||||
$extension = $twig->getExtension(TranslationExtension::class);
|
||||
/** @var LocaleAwareInterface $translator */
|
||||
$translator = $extension->getTranslator();
|
||||
$previousLocale = $translator->getLocale();
|
||||
|
||||
$translator->setLocale($locale);
|
||||
|
||||
/** @var LocaleFormatExtensions $extension */
|
||||
$extension = $twig->getExtension(LocaleFormatExtensions::class);
|
||||
$extension->setLocale($locale);
|
||||
|
||||
return $previousLocale;
|
||||
return $this->renderTwigTemplateWithLanguage($this->twig, $template, $options, $language, $formatLocale);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ use App\Constants;
|
|||
use App\Entity\Invoice;
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Event\InvoiceCreatedEvent;
|
||||
use App\Event\InvoiceDeleteEvent;
|
||||
use App\Event\InvoicePostRenderEvent;
|
||||
use App\Event\InvoicePreRenderEvent;
|
||||
use App\Repository\InvoiceDocumentRepository;
|
||||
|
@ -418,12 +419,17 @@ final class ServiceInvoice
|
|||
return $this->createInvoiceFromModel($model, $dispatcher);
|
||||
}
|
||||
|
||||
public function deleteInvoice(Invoice $invoice)
|
||||
public function deleteInvoice(Invoice $invoice, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$invoiceDirectory = $this->getInvoicesDirectory();
|
||||
|
||||
if (is_file($invoiceDirectory . $invoice->getInvoiceFilename())) {
|
||||
$this->fileHelper->removeFile($invoiceDirectory . $invoice->getInvoiceFilename());
|
||||
}
|
||||
|
||||
$event = new InvoiceDeleteEvent($invoice);
|
||||
$dispatcher->dispatch($event);
|
||||
|
||||
$this->invoiceRepository->deleteInvoice($invoice);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ abstract class AbstractPluginExtension extends Extension
|
|||
{
|
||||
$bundleConfig = [$this->getAlias() => $configs];
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
if ($container->hasParameter('kimai.bundles.config')) {
|
||||
$bundleConfig = array_merge(
|
||||
$container->getParameter('kimai.bundles.config'),
|
||||
|
|
|
@ -51,6 +51,11 @@ class ConfigurationRepository extends EntityRepository implements ConfigLoaderIn
|
|||
self::$initialized = true;
|
||||
}
|
||||
|
||||
public function getConfigurationByName(string $name): ?Configuration
|
||||
{
|
||||
return $this->findOneBy(['name' => $name]);
|
||||
}
|
||||
|
||||
public function saveConfiguration(Configuration $configuration)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
|
@ -59,6 +64,14 @@ class ConfigurationRepository extends EntityRepository implements ConfigLoaderIn
|
|||
$this->clearCache();
|
||||
}
|
||||
|
||||
public function deleteConfiguration(Configuration $configuration)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($configuration);
|
||||
$entityManager->flush();
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prefix
|
||||
* @return Configuration[]
|
||||
|
|
|
@ -137,7 +137,7 @@ final class LocaleFormatExtensions extends AbstractExtension
|
|||
return $this->formatter;
|
||||
}
|
||||
|
||||
private function getLocale()
|
||||
public function getLocale(): string
|
||||
{
|
||||
if (null === $this->locale) {
|
||||
$this->locale = \Locale::getDefault();
|
||||
|
|
66
src/Twig/TwigRendererTrait.php
Normal file
66
src/Twig/TwigRendererTrait.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?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\Twig;
|
||||
|
||||
use Symfony\Bridge\Twig\Extension\TranslationExtension;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait TwigRendererTrait
|
||||
{
|
||||
protected function renderTwigTemplateWithLanguage(Environment $twig, string $template, array $options = [], ?string $language = null, ?string $formatLocale = null): string
|
||||
{
|
||||
$previousTranslation = null;
|
||||
$previousFormatLocale = null;
|
||||
|
||||
if ($language !== null) {
|
||||
$previousTranslation = $this->switchTranslationLocale($twig, $language);
|
||||
}
|
||||
if ($formatLocale !== null) {
|
||||
$previousFormatLocale = $this->switchFormatLocale($twig, $formatLocale);
|
||||
}
|
||||
|
||||
$content = $twig->render($template, $options);
|
||||
|
||||
if ($previousTranslation !== null) {
|
||||
$this->switchTranslationLocale($twig, $previousTranslation);
|
||||
}
|
||||
if ($previousFormatLocale !== null) {
|
||||
$this->switchFormatLocale($twig, $previousFormatLocale);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function switchTranslationLocale(Environment $twig, string $language): string
|
||||
{
|
||||
/** @var TranslationExtension $extension */
|
||||
$extension = $twig->getExtension(TranslationExtension::class);
|
||||
/** @var LocaleAwareInterface $translator */
|
||||
$translator = $extension->getTranslator();
|
||||
$previous = $translator->getLocale();
|
||||
$translator->setLocale($language);
|
||||
|
||||
return $previous;
|
||||
}
|
||||
|
||||
protected function switchFormatLocale(Environment $twig, string $language): string
|
||||
{
|
||||
/** @var LocaleFormatExtensions $extension */
|
||||
$extension = $twig->getExtension(LocaleFormatExtensions::class);
|
||||
$previous = $extension->getLocale();
|
||||
$extension->setLocale($language);
|
||||
|
||||
return $previous;
|
||||
}
|
||||
}
|
|
@ -12,10 +12,13 @@ namespace App\Utils;
|
|||
interface HtmlToPdfConverter
|
||||
{
|
||||
/**
|
||||
* Returns the binary content of the PDF, which can be saved as file or send via Reponse.
|
||||
* Returns the binary content of the PDF, which can be saved as file.
|
||||
* Throws an exception if conversion fails.
|
||||
*
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return mixed
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function convertToPdf(string $html, array $options = []);
|
||||
public function convertToPdf(string $html, array $options = []): string;
|
||||
}
|
||||
|
|
|
@ -56,10 +56,10 @@ class MPdfConverter implements HtmlToPdfConverter
|
|||
/**
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return mixed|string
|
||||
* @return string
|
||||
* @throws \Mpdf\MpdfException
|
||||
*/
|
||||
public function convertToPdf(string $html, array $options = [])
|
||||
public function convertToPdf(string $html, array $options = []): string
|
||||
{
|
||||
$options = array_merge(
|
||||
$this->sanitizeOptions($options),
|
||||
|
|
|
@ -89,6 +89,10 @@ final class CustomerVoter extends Voter
|
|||
}
|
||||
}
|
||||
|
||||
if ($user->canSeeAllData()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,16 +3,18 @@
|
|||
{% set showRateColumn = showRateColumn ?? is_granted('view_rate_other_timesheet') %}
|
||||
{% set showRateBudget = showRateBudget ?? false %}
|
||||
{% set showTimeBudget = showTimeBudget ?? false %}
|
||||
{% set showCustomerSummary = showCustomerSummary ?? true %}
|
||||
{% set showTotalSummary = showTotalSummary ?? true %}
|
||||
{% set showDateTimeShort = showDateTimeShort ?? false %}
|
||||
{% set now = create_date('now', app.user) %}
|
||||
{% set decimal = decimal ?? false %}
|
||||
{# this is only triggered, if a user exports from his personal timesheet screen #}
|
||||
{% if query.user is not null %}
|
||||
{% if query is defined and query.user is not null %}
|
||||
{% set showUserColumn = false %}
|
||||
{# if exporting via the admin screen, users without view_rate_own_timesheet might still see their own rates #}
|
||||
{% set showRateColumn = is_granted('view_rate_own_timesheet') %}
|
||||
{% endif %}
|
||||
<html lang="{{ app.request.locale }}">
|
||||
<html{% if app.request is defined and app.request is not null %} lang="{{ app.request.locale }}"{% endif %}>
|
||||
<head>
|
||||
{% block styles %}
|
||||
<style>
|
||||
|
@ -88,7 +90,7 @@
|
|||
<tr>
|
||||
<td align="left">
|
||||
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
|
||||
{% if not showUserColumn and query.user %}
|
||||
{% if not showUserColumn and query is defined and query.user %}
|
||||
–
|
||||
{{ 'label.user'|trans }}: {{ query.user.displayName }}
|
||||
{% endif %}
|
||||
|
@ -108,11 +110,13 @@
|
|||
mpdf-->
|
||||
{% endblock %}
|
||||
{% block summary %}
|
||||
<h2 style="margin-bottom: 0; padding-bottom: 0">{{ 'export.document_title'|trans }}</h2>
|
||||
<h2 style="margin-bottom: 4px; padding-bottom: 0">{% block title %}{{ 'export.document_title'|trans }}{% endblock %}</h2>
|
||||
{% if query is defined %}
|
||||
<p>
|
||||
{{ 'export.period'|trans }}:
|
||||
{{ query.begin|date_short }} - {{ query.end|date_short }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<h3>{{ 'export.summary'|trans }}</h3>
|
||||
<table class="items">
|
||||
<thead>
|
||||
|
@ -229,7 +233,7 @@ mpdf-->
|
|||
{% set customerInternalRate = customerInternalRate + summary.rate_internal %}
|
||||
{% set customerCount = customerCount + 1 %}
|
||||
{% endfor %}
|
||||
{% if customer is not same as(null) %}
|
||||
{% if showCustomerSummary and customer is not same as(null) %}
|
||||
<tr class="summary">
|
||||
<td colspan="3"></td>
|
||||
{% if showTimeBudget %}
|
||||
|
@ -308,21 +312,22 @@ mpdf-->
|
|||
{% endif %}
|
||||
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
||||
<td class="text-nowrap">
|
||||
{{ entry.begin|date_time }}
|
||||
{% block date_begin %}{% if not showDateTimeShort %}{{ entry.begin|date_time }}{% else %}{{ entry.begin|date_short }}{% endif %}{% endblock %}
|
||||
{% if entry.end %}
|
||||
<br>
|
||||
{{ entry.end|date_time }}
|
||||
{% block date_end %}{% if not showDateTimeShort %}<br>{{ entry.end|date_time }}{% endif %}{% endblock %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if showUserColumn %}
|
||||
<td>{{ entry.user.displayName }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %}
|
||||
{% if entry.description is not empty %}
|
||||
<br>
|
||||
<i>{{ entry.description|desc2html }}</i>
|
||||
{% endif %}
|
||||
{% block description %}
|
||||
{{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %}
|
||||
{% if entry.description is not empty %}
|
||||
<br>
|
||||
<i>{{ entry.description|desc2html }}</i>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</td>
|
||||
<td class="duration">{{ entry.duration|duration(decimal) }}</td>
|
||||
{% if showRateColumn %}
|
||||
|
@ -341,9 +346,9 @@ mpdf-->
|
|||
{% endfor %}
|
||||
<tr class="summary">
|
||||
{% if showUserColumn %}
|
||||
<td colspan="3"></td>
|
||||
<td colspan="3">{{ 'sum.total'|trans }}</td>
|
||||
{% else %}
|
||||
<td colspan="2"></td>
|
||||
<td colspan="2">{{ 'sum.total'|trans }}</td>
|
||||
{% endif %}
|
||||
<td class="totals duration">{{ duration|duration(decimal) }}</td>
|
||||
{% if showRateColumn %}
|
||||
|
|
18
templates/export/renderer/timesheet.pdf.twig
Normal file
18
templates/export/renderer/timesheet.pdf.twig
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends 'export/pdf-layout.html.twig' %}
|
||||
{% set showRateColumn = false %}
|
||||
{% set showRateBudget = false %}
|
||||
{% set showTimeBudget = false %}
|
||||
{% set showUserColumn = true %}
|
||||
{% set showCustomerSummary = false %}
|
||||
{% set decimal = true %}
|
||||
{% set showDateTimeShort = true %}
|
||||
{% block title %}{{ 'timesheet.pdf.twig'|trans({}, 'export') }}{% endblock %}
|
||||
{% block description %}
|
||||
{% if entry.description is not empty %}
|
||||
{{ entry.description|desc2html }}
|
||||
{% elseif entry.activity is not null %}
|
||||
{{ entry.activity.name }}
|
||||
{% endif %}
|
||||
<br>
|
||||
<small>{{ entry.project.customer.name }}, {{ entry.project.name }}</small>
|
||||
{% endblock %}
|
|
@ -25,7 +25,7 @@
|
|||
{% set tableName = 'plugins' %}
|
||||
|
||||
{{ tables.datatable_header(tableName, columns, null, {}) }}
|
||||
{% for plugin in plugins %}
|
||||
{% for plugin in plugins|sort((p1, p2) => p1.name|lower <=> p2.name|lower) %}
|
||||
<tr>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">
|
||||
{% if plugin.id != plugin.name %}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
{% set tableName = tableName|default('project_details_reporting') %}
|
||||
{% set tableId = 'project-details-form' %}
|
||||
{% set view_revenue_tab = project_details is not null and is_granted('budget', project_details.project) %}
|
||||
{% set view_revenue_user = project_details is not null and is_granted('view_rate_other_timesheet') %}
|
||||
{% set view_budget = project_details is not null and is_granted('budget', project_details.project) %}
|
||||
{% set view_revenue = project_details is not null and is_granted('view_rate_other_timesheet') %}
|
||||
{% set see_users = is_granted('view_other_timesheet') or is_granted('view_other_reporting') %}
|
||||
|
||||
{% block stylesheets %}
|
||||
|
@ -23,7 +23,7 @@
|
|||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% set options = {'label': 'duration', 'title': 'name', 'legend': 'false'} %}
|
||||
{% if view_revenue_user and view_revenue_tab %}
|
||||
{% if view_revenue %}
|
||||
{% set options = options|merge({'footer': 'rate'}) %}
|
||||
{% endif %}
|
||||
{{ charts.doughnut_javascript(options) }}
|
||||
|
@ -79,17 +79,17 @@
|
|||
{% endif %}
|
||||
|
||||
{% if hasData %}
|
||||
{{ _self.project_details(project, project_view, project_details, view_revenue_tab, view_revenue_user, see_users) }}
|
||||
{{ _self.project_details(project, project_view, project_details, view_budget, view_revenue, see_users) }}
|
||||
{% set currency = project.customer.currency %}
|
||||
|
||||
{%- for yearStat in project_details.years|reverse %}
|
||||
{% set year = yearStat.year %}
|
||||
{{ _self.duration_stat(year, year, year, month_names(), yearStat, project_details.getYearActivities(year), project_details.userYears(year), currency, view_revenue_tab, see_users) }}
|
||||
{{ _self.duration_stat(year, year, year, month_names(), yearStat, project_details.getYearActivities(year), project_details.userYears(year), currency, view_revenue, see_users) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% macro duration_stat(id, title, year, labels, yearStat, activities, users, currency, view_revenue_tab, see_users) %}
|
||||
{% macro duration_stat(id, title, year, labels, yearStat, activities, users, currency, view_revenue, see_users) %}
|
||||
{% set rates = [] %}
|
||||
{% set durations = [] %}
|
||||
{% set chartPrefix = 'chart' ~ id %}
|
||||
|
@ -111,11 +111,11 @@
|
|||
<li><a data-chart="{{ chartPrefix }}User" href="#user-chart-{{ id}}" data-toggle="tab">{{ 'label.user'|trans }}</a></li>
|
||||
{% endif %}
|
||||
<li><a data-chart="{{ chartPrefix }}Activity" href="#activity-chart-{{ id}}" data-toggle="tab">{{ 'label.activity'|trans }}</a></li>
|
||||
{% if view_revenue_tab %}
|
||||
{% if view_revenue %}
|
||||
<li><a data-chart="{{ chartPrefix }}Rate" href="#revenue-chart-{{ id }}" data-toggle="tab">{{ 'stats.revenue'|trans }}</a></li>
|
||||
{% endif %}
|
||||
<li class="active"><a data-chart="{{ chartPrefix }}Duration" href="#time-chart-{{ id }}" data-toggle="tab">{{ 'stats.workingTime'|trans }}</a></li>
|
||||
<li class="pull-left header">{{ title }} {{ widgets.label(yearStat.duration|duration, 'gray') }} {{ widgets.label(yearStat.rate|money(currency), 'gray') }}</small></li>
|
||||
<li class="pull-left header">{{ title }} {{ widgets.label(yearStat.duration|duration, 'gray') }}{% if view_revenue %} {{ widgets.label(yearStat.rate|money(currency), 'gray') }}{% endif %}</small></li>
|
||||
</ul>
|
||||
<div class="tab-content no-padding">
|
||||
<div class="chart tab-pane active" id="time-chart-{{ id }}">
|
||||
|
@ -125,7 +125,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if view_revenue_tab %}
|
||||
{% if view_revenue %}
|
||||
<div class="chart tab-pane" id="revenue-chart-{{ id }}">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
|
@ -135,7 +135,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<div class="chart tab-pane" id="activity-chart-{{ id }}">
|
||||
{{ _self.activity_tab(activities, yearStat.duration, currency, chartPrefix, view_revenue_tab) }}
|
||||
{{ _self.activity_tab(activities, yearStat.duration, currency, chartPrefix, view_revenue) }}
|
||||
</div>
|
||||
{% if see_users %}
|
||||
<div class="chart tab-pane" id="user-chart-{{ id }}">
|
||||
|
@ -208,9 +208,9 @@
|
|||
totalDuration = int
|
||||
currency = string
|
||||
chartPrefix = string
|
||||
view_revenue_tab = boolean
|
||||
view_revenue = boolean
|
||||
#}
|
||||
{% macro activity_tab(activities, totalDuration, currency, chartPrefix, view_revenue_tab) %}
|
||||
{% macro activity_tab(activities, totalDuration, currency, chartPrefix, view_revenue) %}
|
||||
{% set dataset = [] %}
|
||||
{% set labels = [] %}
|
||||
<div class="row">
|
||||
|
@ -225,7 +225,7 @@
|
|||
<tr>
|
||||
<td>{{ widgets.label_activity(stat.activity, {'inherit': false, 'random': true}) }}</td>
|
||||
<td class="text-nowrap text-right">{{ stat.duration|duration }}</td>
|
||||
{% if view_revenue_tab %}
|
||||
{% if view_revenue %}
|
||||
<td class="text-nowrap text-right">{{ stat.rate|money(currency) }}</td>
|
||||
{% endif %}
|
||||
<td class="text-nowrap text-right">{{ percentage|number_format(1) }} %</td>
|
||||
|
@ -245,7 +245,7 @@
|
|||
project_view = ProjectViewModel
|
||||
project_details = ProjectDetailsModel
|
||||
#}
|
||||
{% macro project_details(project, project_view, project_details, view_revenue_tab, view_revenue_user, see_users) %}
|
||||
{% macro project_details(project, project_view, project_details, view_budget, view_revenue, see_users) %}
|
||||
{% set activities = project_details.activities %}
|
||||
{% set years = project_details.years %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
|
@ -271,7 +271,7 @@
|
|||
{% if activities is not empty %}
|
||||
<li><a data-chart="{{ chartPrefix }}Activity" href="#activity-chart" data-toggle="tab">{{ 'label.activity'|trans }}</a></li>
|
||||
{% endif %}
|
||||
{% if view_revenue_tab and project_view.rateTotal > 0 %}
|
||||
{% if view_revenue and project_view.rateTotal > 0 %}
|
||||
<li><a data-chart="{{ chartPrefix }}Rate" href="#revenue-chart" data-toggle="tab">{{ 'stats.revenue'|trans }}</a></li>
|
||||
{% endif %}
|
||||
{% if project_view.durationTotal > 0 %}
|
||||
|
@ -301,7 +301,7 @@
|
|||
</th>
|
||||
<td>{{ project_view.durationTotal|duration }}</td>
|
||||
</tr>
|
||||
{% if view_revenue_tab %}
|
||||
{% if view_revenue %}
|
||||
<tr>
|
||||
<th class="w-min">
|
||||
{{ 'stats.amountTotal'|trans }}
|
||||
|
@ -352,7 +352,7 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% if view_revenue_tab and (project.timeBudget > 0 or project.budget > 0) %}
|
||||
{% if view_budget and (project.timeBudget > 0 or project.budget > 0) %}
|
||||
{% set budgetStats = project_details.budgetStatisticModel %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
|
@ -389,7 +389,7 @@
|
|||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% if view_revenue_tab and project_view.rateTotal > 0 %}
|
||||
{% if view_revenue and project_view.rateTotal > 0 %}
|
||||
<div class="chart tab-pane" id="revenue-chart">
|
||||
{{ charts.bar_chart(chartPrefix ~ 'Rate', labels, [rates], {'height': '300px', 'renderEvent': 'render.' ~ chartPrefix ~ 'Rate'}) }}
|
||||
</div>
|
||||
|
@ -401,7 +401,7 @@
|
|||
{% endif %}
|
||||
{% if activities is not empty %}
|
||||
<div class="chart tab-pane" id="activity-chart">
|
||||
{{ _self.activity_tab(activities, project_view.durationTotal, project.customer.currency, chartPrefix, view_revenue_tab) }}
|
||||
{{ _self.activity_tab(activities, project_view.durationTotal, project.customer.currency, chartPrefix, view_revenue) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if see_users and project_details.userStats|length > 0 %}
|
||||
|
@ -430,7 +430,7 @@
|
|||
<td class="text-nowrap text-right">
|
||||
{{ userStat.duration|duration }}
|
||||
</td>
|
||||
{% if view_revenue_user %}
|
||||
{% if view_revenue %}
|
||||
<td class="text-nowrap text-right">
|
||||
{{ userStat.rate|money(currency) }}
|
||||
</td>
|
||||
|
@ -443,7 +443,7 @@
|
|||
<tr>
|
||||
<td></td>
|
||||
<th class="text-nowrap text-right total">{{ totalDuration|duration }}</th>
|
||||
{% if view_revenue_user %}
|
||||
{% if view_revenue %}
|
||||
<th class="text-nowrap text-right total">{{ rateTotal|money(currency) }}</th>
|
||||
{% endif %}
|
||||
<td></td>
|
||||
|
|
|
@ -170,6 +170,13 @@ class TeamControllerTest extends APIControllerBaseTest
|
|||
$this->assertNotEmpty($result['id']);
|
||||
self::assertCount(3, $result['users']);
|
||||
|
||||
$this->request($client, '/api/teams/' . $updateId);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('TeamEntity', $result);
|
||||
self::assertCount(3, $result['users']);
|
||||
|
||||
self::assertFalse($result['members'][1]['teamlead']);
|
||||
self::assertEquals(1, $result['members'][1]['user']['id']);
|
||||
self::assertEquals('clara_customer', $result['members'][1]['user']['username']);
|
||||
|
|
|
@ -31,6 +31,7 @@ class ActivateUserCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -31,6 +31,7 @@ class ChangePasswordCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -30,6 +30,7 @@ class CreateUserCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -31,6 +31,7 @@ class DeactivateUserCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -32,6 +32,7 @@ class DemoteUserCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -27,6 +27,7 @@ class ImportCustomerCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -29,6 +29,7 @@ class ImportProjectCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -34,6 +34,7 @@ class ImportTimesheetCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ class InstallCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -61,6 +61,7 @@ class InvoiceCreateCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->clearInvoiceFiles();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
|
|
|
@ -29,6 +29,7 @@ class KimaiImporterCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ class PromoteUserCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
|
|
@ -26,6 +26,7 @@ class ReloadCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$this->application->add(new ReloadCommand());
|
||||
|
|
|
@ -28,6 +28,7 @@ class VersionCommandTest extends KernelTestCase
|
|||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
|
||||
|
|
|
@ -92,6 +92,9 @@ class TeamTest extends TestCase
|
|||
self::assertCount(0, $sut->getMembers());
|
||||
self::assertFalse($sut->isTeamlead($user));
|
||||
|
||||
$member = new TeamMember();
|
||||
$member->setUser($user);
|
||||
|
||||
$sut->addMember($member);
|
||||
$member->setTeamlead(true);
|
||||
self::assertTrue($sut->isTeamlead($user));
|
||||
|
|
|
@ -46,10 +46,10 @@ class UserPreferenceTest extends TestCase
|
|||
$sut->setType(IntegerType::class);
|
||||
self::assertSame(1, $sut->getValue());
|
||||
$sut->setType(YesNoType::class);
|
||||
self::assertSame(true, $sut->getValue());
|
||||
self::assertTrue($sut->getValue());
|
||||
$sut->setValue('0');
|
||||
$sut->setType(CheckboxType::class);
|
||||
self::assertSame(false, $sut->getValue());
|
||||
self::assertFalse($sut->getValue());
|
||||
}
|
||||
|
||||
public function testGetLabelWithLabelOption()
|
||||
|
|
|
@ -449,6 +449,9 @@ class UserTest extends TestCase
|
|||
|
||||
self::assertFalse($sut->isTeamleadOf($team));
|
||||
|
||||
$member = new TeamMember();
|
||||
$member->setTeam($team);
|
||||
|
||||
$sut->addMembership($member);
|
||||
self::assertCount(1, $sut->getMemberships());
|
||||
self::assertFalse($sut->isTeamleadOf($team));
|
||||
|
|
29
tests/Event/InvoiceDeleteEventTest.php
Normal file
29
tests/Event/InvoiceDeleteEventTest.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?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\Event;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use App\Event\InvoiceDeleteEvent;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\InvoiceDeleteEvent
|
||||
*/
|
||||
class InvoiceDeleteEventTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
|
||||
$sut = new InvoiceDeleteEvent($invoice);
|
||||
|
||||
self::assertSame($invoice, $sut->getInvoice());
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ abstract class AbstractRepositoryTest extends KernelTestCase
|
|||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
|
||||
$this->entityManager = $kernel->getContainer()
|
||||
|
|
19
tests/phpstan-doctrine.php
Normal file
19
tests/phpstan-doctrine.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?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.
|
||||
*/
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
(new Dotenv(false))->loadEnv(dirname(__DIR__) . '/.env');
|
||||
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||
$kernel->boot();
|
||||
|
||||
return $kernel->getContainer()->get('doctrine')->getManager();
|
|
@ -1,7 +1,10 @@
|
|||
includes:
|
||||
- %rootDir%/../phpstan-symfony/extension.neon
|
||||
- %rootDir%/../phpstan-symfony/rules.neon
|
||||
- %rootDir%/../phpstan-doctrine/extension.neon
|
||||
- %rootDir%/../phpstan-doctrine/rules.neon
|
||||
- %rootDir%/../phpstan-phpunit/extension.neon
|
||||
- %rootDir%/../phpstan-phpunit/rules.neon
|
||||
|
||||
parameters:
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
|
@ -9,4 +12,6 @@ parameters:
|
|||
- '#Call to static method PHPUnit\\Framework\\Assert::assertSame\(\) with App\\Entity\\[a-zA-Z0-9]+ and null will always evaluate to false.#'
|
||||
excludePaths:
|
||||
- %rootDir%/../../../tests/Ldap/LdapDriverTest.php
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
doctrine:
|
||||
objectManagerLoader: %rootDir%/../../../tests/phpstan-doctrine.php
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</trans-unit>
|
||||
<trans-unit id="A0nWoXZ" resname="special_thanks">
|
||||
<source>special_thanks</source>
|
||||
<target>これらの方々に特に感謝いたします。</target>
|
||||
<target>に深く感謝します</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="L7cff3Q" resname="library_authors">
|
||||
<source>library_authors</source>
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
<source>default-internal.pdf.twig</source>
|
||||
<target>Inkl. interner Kosten</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bTIKMV." resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Stundennachweis</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -2,18 +2,22 @@
|
|||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="en" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="oRRnhwf" resname="default.pdf.twig">
|
||||
<source>default.pdf.twig</source>
|
||||
<target>Standard</target>
|
||||
<trans-unit id="IbERy.5" resname="default.pdf.twig">
|
||||
<source>Default</source>
|
||||
<target>Default</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YjWg9hr" resname="default-budget.pdf.twig">
|
||||
<source>default-budget.pdf.twig</source>
|
||||
<trans-unit id="Xvh5B5U" resname="default-budget.pdf.twig">
|
||||
<source>With remaining budget</source>
|
||||
<target>With remaining budget</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="AuhL8Sb" resname="default-internal.pdf.twig">
|
||||
<source>default-internal.pdf.twig</source>
|
||||
<trans-unit id="rok3rBO" resname="default-internal.pdf.twig">
|
||||
<source>With internal rates</source>
|
||||
<target>With internal rates</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bTIKMV." resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Timesheet</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="he" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="oRRnhwf" resname="default.pdf.twig">
|
||||
<source>default.pdf.twig</source>
|
||||
<target>סטנדרטי</target>
|
||||
<source>Default</source>
|
||||
<target>ברירת מחדל</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YjWg9hr" resname="default-budget.pdf.twig">
|
||||
<source>default-budget.pdf.twig</source>
|
||||
|
@ -14,6 +14,10 @@
|
|||
<source>default-internal.pdf.twig</source>
|
||||
<target>עם תעריפים פנימיים</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bTIKMV." resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>גיליון שעות</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="pt-BR" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="oRRnhwf" resname="default.pdf.twig">
|
||||
<source>default.pdf.twig</source>
|
||||
<source>Default</source>
|
||||
<target>Padrão</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YjWg9hr" resname="default-budget.pdf.twig">
|
||||
|
@ -14,6 +14,10 @@
|
|||
<source>default-internal.pdf.twig</source>
|
||||
<target>Com as taxas internas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bTIKMV." resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Planilha de horários</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
10
translations/export.ro.xlf
Executable file → Normal file
10
translations/export.ro.xlf
Executable file → Normal file
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="ro" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="oRRnhwf" resname="default.pdf.twig">
|
||||
<source>default.pdf.twig</source>
|
||||
<target>Standard</target>
|
||||
<source>Default</source>
|
||||
<target>Implicit</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YjWg9hr" resname="default-budget.pdf.twig">
|
||||
<source>default-budget.pdf.twig</source>
|
||||
|
@ -14,6 +14,10 @@
|
|||
<source>default-internal.pdf.twig</source>
|
||||
<target>Cu tarifele interne</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bTIKMV." resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Fișă de pontaj</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<file source-language="en" target-language="tr" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="oRRnhwf" resname="default.pdf.twig">
|
||||
<source>default.pdf.twig</source>
|
||||
<target>Standart</target>
|
||||
<source>Default</source>
|
||||
<target>Öntanımlı</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YjWg9hr" resname="default-budget.pdf.twig">
|
||||
<source>default-budget.pdf.twig</source>
|
||||
|
@ -14,6 +14,10 @@
|
|||
<source>default-internal.pdf.twig</source>
|
||||
<target>Dahili ücretlerle</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bTIKMV." resname="timesheet.pdf.twig">
|
||||
<source>Timesheet</source>
|
||||
<target>Zaman çizelgesi</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="zh-CN" datatype="plaintext" original="export.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="oRRnhwf" resname="default.pdf.twig">
|
||||
<source>default.pdf.twig</source>
|
||||
<target>标准</target>
|
||||
<source>Default</source>
|
||||
<target>默认</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YjWg9hr" resname="default-budget.pdf.twig">
|
||||
<source>default-budget.pdf.twig</source>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fa" datatype="plaintext" original="messages.en.xlf">
|
||||
<body>
|
||||
|
@ -427,6 +427,141 @@
|
|||
<source>label.searchTerm</source>
|
||||
<target>عبارت جستجو</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Zpj9UB4" resname="label.teamlead">
|
||||
<source>label.teamlead</source>
|
||||
<target>رهبری تیم</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="trDJK9W" resname="label.begin">
|
||||
<source>label.begin</source>
|
||||
<target>از</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="R9typUt" resname="label.end">
|
||||
<source>label.end</source>
|
||||
<target>به</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="egaNXjp" resname="label.daterange">
|
||||
<source>label.daterange</source>
|
||||
<target>محدوده زمانی</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YTSS_Q1" resname="label.color">
|
||||
<source>label.color</source>
|
||||
<target>رنگ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="GzWKgBk" resname="error.directory_missing">
|
||||
<source>error.directory_missing</source>
|
||||
<target>موقعیت "%dir%" وجود ندارد و قابل ساختن نیست.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="C2TGff0" resname="error.directory_protected">
|
||||
<source>error.directory_protected</source>
|
||||
<target>موقعیت "%dir%" در برابر write حفاظت شده است.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="jdbtx6z" resname="action.add">
|
||||
<source>action.add</source>
|
||||
<target>افزودن</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="b43sCwO" resname="help.fixedRate">
|
||||
<source>help.fixedRate</source>
|
||||
<target>هربار سابقه مقدار معینی میگیرد، صرف نظر از مدت آن</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="EoouxGX" resname="label.replaceTags">
|
||||
<source>label.replaceTags</source>
|
||||
<target>جایگزینی برچسب ها</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="BxkmaKz" resname="label.appendTags">
|
||||
<source>label.appendTags</source>
|
||||
<target>پیوست تگ ها</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1_wnK76" resname="profile.title">
|
||||
<source>profile.title</source>
|
||||
<target>نمایه کاربر</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="HWKJZ0T" resname="profile.about_me">
|
||||
<source>profile.about_me</source>
|
||||
<target>درباره من</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1c0EQaz" resname="profile.registration_date">
|
||||
<source>profile.registration_date</source>
|
||||
<target>ثبت شده در</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="A6TLLQa" resname="profile.password">
|
||||
<source>profile.password</source>
|
||||
<target>رمز</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Z34ZpjK" resname="profile.api-token">
|
||||
<source>profile.api-token</source>
|
||||
<target>رابط برنامه نویسی اپلیکیشن</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="ygtTz8." resname="profile.roles">
|
||||
<source>profile.roles</source>
|
||||
<target>نقش ها</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="DZ7XGML" resname="profile.teams">
|
||||
<source>profile.teams</source>
|
||||
<target>تیم ها</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="MQKiG33" resname="profile.preferences">
|
||||
<source>profile.preferences</source>
|
||||
<target>الویت ها</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="iqQsgIW" resname="label.login.initial_view">
|
||||
<source>label.login.initial_view</source>
|
||||
<target>نمایه ابتدایی بعد از ورود</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="hO7mkmf" resname="label.lastLogin">
|
||||
<source>label.lastLogin</source>
|
||||
<target>آخرین ورود</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Eq78QrH" resname="label.theme.collapsed_sidebar">
|
||||
<source>label.theme.collapsed_sidebar</source>
|
||||
<target>کوچک کردن نوار کناری سمت چپ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="0sgroZi" resname="label.calendar.initial_view">
|
||||
<source>label.calendar.initial_view</source>
|
||||
<target>مشاهده ابتدایی و ساده تقویم</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dwEVXUR" resname="timesheet.edit">
|
||||
<source>timesheet.edit</source>
|
||||
<target>ویرایش سابقه.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="UbnqJLn" resname="modal.columns.title">
|
||||
<source>modal.columns.title</source>
|
||||
<target>ویرایش پدیداری ستون</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="xEYQXPy" resname="profile.settings">
|
||||
<source>profile.settings</source>
|
||||
<target>نمایه</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="NNKq04n" resname="label.remove_default">
|
||||
<source>label.remove_default</source>
|
||||
<target>حذف جستجوی علاقه مندی</target>
|
||||
</trans-unit>
|
||||
<trans-unit id=".ndupSK" resname="registration.check_email">
|
||||
<source>registration.check_email</source>
|
||||
<target>ایمیلی برای %email% شامل لینک فعالسازی ارسال شد. جهت فعال کردن حساب کاربری خود باید بر روی آن کلیک کنید.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="_cY.wHP" resname="resetting.check_email">
|
||||
<source>resetting.check_email</source>
|
||||
<target>لینکی به ایمیل شما ارسال شد. جهت بازیابی رمز خود روی آن کلیک کنید. نکته: شما در طی %tokenLifetime% ساعت فقط یکبار میتوانید رمز جدید درخواست کنید.
|
||||
اگر ایمیل برای شما ارسال نشد پوشه spam را بررسی کنید یا دوباره تلاش کنید.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="XQ2.pq2" resname="error.too_many_entries">
|
||||
<source>error.too_many_entries</source>
|
||||
<target>درخواست قابل انجام نیست.
|
||||
نتایج زیادی پیدا شدند.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5gjqxgK" resname="label.fixedRate">
|
||||
<source>label.fixedRate</source>
|
||||
<target>نرخ ثابت</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="EbfIoLp" resname="modal.columns.description">
|
||||
<source>modal.columns.description</source>
|
||||
<target>ذخیره ستون های علامت گذاری نشده مخفی خواهد بود و این تنظیمات در کوکی مرورگر ذخیره میشود.
|
||||
اگر کوکی ها را پاک کنید تنظیمات به حالت اولیه بازمیگردند.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Fm.kwVn" resname="profile.first_entry">
|
||||
<source>profile.first_entry</source>
|
||||
<target>کار میکند از تاریخ</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -975,6 +975,70 @@
|
|||
<source>label.hours_24</source>
|
||||
<target>24 tímar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5gjqxgK" resname="label.fixedRate">
|
||||
<source>label.fixedRate</source>
|
||||
<target>Fastur kostnáður</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Dgmh_mv" resname="label.vat_id">
|
||||
<source>label.vat_id</source>
|
||||
<target>V-tal</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="F6Jmro7" resname="export.title">
|
||||
<source>export.title</source>
|
||||
<target>Útflyt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="pnGDFmf" resname="label.type">
|
||||
<source>label.type</source>
|
||||
<target>Slag</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="_cY.wHP" resname="resetting.check_email">
|
||||
<source>resetting.check_email</source>
|
||||
<target>Ein teldupostur er sendur. Telduposturin inniheldur eina leinkju, ið tú skalt trýst á, fyri at endurseta títt loyniorð. GG: Tú kanst einans biða um eitt nýtt loyniorð einaferð hvønn %tokenLifetime% tíma. Um tú ikki móttekur ein teldupost, kannað so tína fras mappu ella royn umaftur.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Uvo1CbP" resname="status.pending">
|
||||
<source>status.pending</source>
|
||||
<target>Bíðistøðu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="GzWKgBk" resname="error.directory_missing">
|
||||
<source>error.directory_missing</source>
|
||||
<target>Mappan "%dir%" finninst ikki, og tað bar ikki til at skapa hana.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="C2TGff0" resname="error.directory_protected">
|
||||
<source>error.directory_protected</source>
|
||||
<target>Mappan "%dir%" skrivivard.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="0sgroZi" resname="label.calendar.initial_view">
|
||||
<source>label.calendar.initial_view</source>
|
||||
<target>Byrjanarvísan fyri kalendara</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="iqQsgIW" resname="label.login.initial_view">
|
||||
<source>label.login.initial_view</source>
|
||||
<target>Byrjanarvísan eftir innritan</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5R.QsZ3" resname="theme.update_browser_title">
|
||||
<source>theme.update_browser_title</source>
|
||||
<target>navnabót kagara dagførd</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="tSlqVK2" resname="label.timesheet.export_decimal">
|
||||
<source>label.timesheet.export_decimal</source>
|
||||
<target>Nýt kommatals-tíðarlongd í útflutningi</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="7J7G3Vr" resname="label.mark_as_exported">
|
||||
<source>label.mark_as_exported</source>
|
||||
<target>Merk sum útflutt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="xN7MfSA" resname="stats.percentUsed_month">
|
||||
<source>stats.percentUsed_month</source>
|
||||
<target>%percent%% brúkt hendan mánaðin</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="vWJPpYU" resname="stats.percentUsedLeft_month">
|
||||
<source>stats.percentUsedLeft_month</source>
|
||||
<target>%percent%% brúkt (%left% er enn opið hendan mánaðin)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="XQ2.pq2" resname="error.too_many_entries">
|
||||
<source>error.too_many_entries</source>
|
||||
<target>Umbønin kundi ikki fullfíggjast. Ov nógv úrslit vóru funnin.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -817,6 +817,14 @@
|
|||
<source>label.globalsOnly</source>
|
||||
<target>グローバルオンリー</target>
|
||||
</trans-unit>
|
||||
<trans-unit id=".0CFrRV" resname="upload">
|
||||
<source>upload</source>
|
||||
<target>アップロード</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="JBkykGe" resname="search">
|
||||
<source>search</source>
|
||||
<target>検索</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -1253,6 +1253,14 @@
|
|||
<source>label.budgetType_month</source>
|
||||
<target>월별</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="C2TGff0" resname="error.directory_protected">
|
||||
<source>error.directory_protected</source>
|
||||
<target>Directory "%dir%" is write protected.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="GzWKgBk" resname="error.directory_missing">
|
||||
<source>error.directory_missing</source>
|
||||
<target>Directory "%dir%" is not existing and could not be created.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -943,7 +943,7 @@
|
|||
</trans-unit>
|
||||
<trans-unit id="JIKNAYP" resname="stats.workingTimeMonth">
|
||||
<source>stats.workingTimeMonth</source>
|
||||
<target state="translated">%month% %year%</target>
|
||||
<target state="translated">月,年,%month% %year%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4kQGReV" resname="label.project_end">
|
||||
<source>label.project_end</source>
|
||||
|
@ -1237,6 +1237,38 @@
|
|||
<source>add_user.label</source>
|
||||
<target>添加用户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="tSlqVK2" resname="label.timesheet.export_decimal">
|
||||
<source>label.timesheet.export_decimal</source>
|
||||
<target>在导出中使用十进制持续时间</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wq.h4nD" resname="label.includeBudgetType_month">
|
||||
<source>label.includeBudgetType_month</source>
|
||||
<target>显示带有“每月”预算的条目</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="nVulc7." resname="label.includeBudgetType_full">
|
||||
<source>label.includeBudgetType_full</source>
|
||||
<target>显示包含“生命周期”预算的条目</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="tvV0NhL" resname="delete_warning.short_stats">
|
||||
<source>delete_warning.short_stats</source>
|
||||
<target>当前存在%records%时间记录,总计持续时间为%duration%。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="AJ6dwR." resname="label.last_record_before">
|
||||
<source>label.last_record_before</source>
|
||||
<target>以后没有时间预订</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="9Rhadz6" resname="label.budgetType_full">
|
||||
<source>label.budgetType_full</source>
|
||||
<target>生命周期</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yNWIi5U" resname="default_value_new">
|
||||
<source>default_value_new</source>
|
||||
<target>新条目的默认值</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="nlwMf5w" resname="invoice_document.max_reached">
|
||||
<source>invoice_document.max_reached</source>
|
||||
<target>已达到%max%发票单据的最大金额。删除一个后,可以添加更多。</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -294,6 +294,10 @@
|
|||
<source>label.theme.avatar_url</source>
|
||||
<target>Permitir a utilização de URLs para as imagens do avatar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="zeOPuh6" resname="label.quick_entry.recent_activities">
|
||||
<source>label.quick_entry.recent_activities</source>
|
||||
<target>Quantidade de entradas retomadas das semanas anteriores</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -258,6 +258,18 @@
|
|||
<source>label.theme.tags_create</source>
|
||||
<target>标签:使用自动完成搜索并允许创建标签</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="zeOPuh6" resname="label.quick_entry.recent_activities">
|
||||
<source>label.quick_entry.recent_activities</source>
|
||||
<target>从前几周接收的参赛作品数量</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="SCkhv_z" resname="label.theme.avatar_url">
|
||||
<source>label.theme.avatar_url</source>
|
||||
<target>允许对化身图像使用URL</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="40i4Xlr" resname="label.timesheet.duration_increment">
|
||||
<source>label.timesheet.duration_increment</source>
|
||||
<target>持续时间的分钟选择</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
Loading…
Add table
Reference in a new issue