allow to restrict usage of global activities for projects (#3437)
This commit is contained in:
@@ -252,6 +252,15 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||||||
* @ORM\Column(name="invoice_text", type="text", nullable=true)
|
* @ORM\Column(name="invoice_text", type="text", nullable=true)
|
||||||
*/
|
*/
|
||||||
private $invoiceText;
|
private $invoiceText;
|
||||||
|
/**
|
||||||
|
* Whether this project allows booking of global activities
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="global_activities", type="boolean", nullable=false, options={"default": true})
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $globalActivities = true;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -419,6 +428,16 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isGlobalActivities(): bool
|
||||||
|
{
|
||||||
|
return $this->globalActivities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setGlobalActivities(bool $globalActivities): void
|
||||||
|
{
|
||||||
|
$this->globalActivities = $globalActivities;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection|MetaTableTypeInterface[]
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Entity\Customer;
|
|||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Form\Type\CustomerType;
|
use App\Form\Type\CustomerType;
|
||||||
use App\Form\Type\DateTimePickerType;
|
use App\Form\Type\DateTimePickerType;
|
||||||
|
use App\Form\Type\YesNoType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
@@ -95,7 +96,11 @@ class ProjectEditForm extends AbstractType
|
|||||||
'placeholder' => (null === $id && null === $customer) ? '' : false,
|
'placeholder' => (null === $id && null === $customer) ? '' : false,
|
||||||
'customers' => $customer,
|
'customers' => $customer,
|
||||||
'query_builder_for_user' => true,
|
'query_builder_for_user' => true,
|
||||||
]);
|
])
|
||||||
|
->add('globalActivities', YesNoType::class, [
|
||||||
|
'label' => 'label.globalActivities',
|
||||||
|
])
|
||||||
|
;
|
||||||
|
|
||||||
$this->addCommonFields($builder, $options);
|
$this->addCommonFields($builder, $options);
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/Migrations/Version20220722125847.php
Normal file
38
src/Migrations/Version20220722125847.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 DoctrineMigrations;
|
||||||
|
|
||||||
|
use App\Doctrine\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @version 1.22.0
|
||||||
|
*/
|
||||||
|
final class Version20220722125847 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '(De-)Activate global activities for Projects';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$projects = $schema->getTable('kimai2_projects');
|
||||||
|
$projects->addColumn('global_activities', 'boolean', ['notnull' => true, 'default' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$projects = $schema->getTable('kimai2_projects');
|
||||||
|
$projects->dropColumn('global_activities');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -286,12 +286,28 @@ class ActivityRepository extends EntityRepository
|
|||||||
if ($query->isGlobalsOnly()) {
|
if ($query->isGlobalsOnly()) {
|
||||||
$mainQuery->add($qb->expr()->isNull('a.project'));
|
$mainQuery->add($qb->expr()->isNull('a.project'));
|
||||||
} elseif ($query->hasProjects()) {
|
} elseif ($query->hasProjects()) {
|
||||||
$mainQuery->add(
|
$orX = $qb->expr()->orX(
|
||||||
$qb->expr()->orX(
|
|
||||||
$qb->expr()->isNull('a.project'),
|
|
||||||
$qb->expr()->in('a.project', ':project')
|
$qb->expr()->in('a.project', ':project')
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$includeGlobals = true;
|
||||||
|
// projects have a setting to disallow global activities, and we check for it only
|
||||||
|
// if we query for exactly one project (usually used in dropdown queries)
|
||||||
|
if (\count($query->getProjects()) === 1) {
|
||||||
|
$project = $query->getProjects()[0];
|
||||||
|
if (!$project instanceof Project) {
|
||||||
|
$project = $this->getEntityManager()->getRepository(Project::class)->find($project);
|
||||||
|
}
|
||||||
|
if ($project instanceof Project) {
|
||||||
|
$includeGlobals = $project->isGlobalActivities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($includeGlobals) {
|
||||||
|
$orX->add($qb->expr()->isNull('a.project'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$mainQuery->add($orX);
|
||||||
$qb->setParameter('project', $query->getProjects());
|
$qb->setParameter('project', $query->getProjects());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +397,22 @@ class ActivityRepository extends EntityRepository
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!$query->isExcludeGlobals()) {
|
if (!$query->isExcludeGlobals()) {
|
||||||
|
$includeGlobals = true;
|
||||||
|
// projects have a setting to disallow global activities, and we check for it only
|
||||||
|
// if we query for exactly one project (usually used in dropdown queries)
|
||||||
|
if (\count($query->getProjects()) === 1) {
|
||||||
|
$project = $query->getProjects()[0];
|
||||||
|
if (!$project instanceof Project) {
|
||||||
|
$project = $this->getEntityManager()->getRepository(Project::class)->find($project);
|
||||||
|
}
|
||||||
|
if ($project instanceof Project) {
|
||||||
|
$includeGlobals = $project->isGlobalActivities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($includeGlobals) {
|
||||||
$orX->add($qb->expr()->isNull('a.project'));
|
$orX->add($qb->expr()->isNull('a.project'));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$where->add($orX);
|
$where->add($orX);
|
||||||
$qb->setParameter('project', $query->getProjects());
|
$qb->setParameter('project', $query->getProjects());
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class TimesheetBasic extends TimesheetConstraint
|
|||||||
public const DISABLED_CUSTOMER_ERROR = 'kimai-timesheet-89';
|
public const DISABLED_CUSTOMER_ERROR = 'kimai-timesheet-89';
|
||||||
public const PROJECT_NOT_STARTED = 'kimai-timesheet-91';
|
public const PROJECT_NOT_STARTED = 'kimai-timesheet-91';
|
||||||
public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92';
|
public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92';
|
||||||
|
public const PROJECT_DISALLOWS_GLOBAL_ACTIVITY = 'kimai-timesheet-93';
|
||||||
|
|
||||||
protected static $errorNames = [
|
protected static $errorNames = [
|
||||||
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
|
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
|
||||||
@@ -39,6 +40,7 @@ class TimesheetBasic extends TimesheetConstraint
|
|||||||
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
|
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
|
||||||
self::PROJECT_NOT_STARTED => 'The project has not started at that time.',
|
self::PROJECT_NOT_STARTED => 'The project has not started at that time.',
|
||||||
self::PROJECT_ALREADY_ENDED => 'The project is finished at that time.',
|
self::PROJECT_ALREADY_ENDED => 'The project is finished at that time.',
|
||||||
|
self::PROJECT_DISALLOWS_GLOBAL_ACTIVITY => 'Global activities are forbidden for the selected project.',
|
||||||
];
|
];
|
||||||
|
|
||||||
public $message = 'This timesheet has invalid settings.';
|
public $message = 'This timesheet has invalid settings.';
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ final class TimesheetBasicValidator extends ConstraintValidator
|
|||||||
->addViolation();
|
->addViolation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$project->isGlobalActivities() && $activity->isGlobal()) {
|
||||||
|
$context->buildViolation('Global activities are forbidden for the selected project.')
|
||||||
|
->atPath('activity')
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode(TimesheetBasic::PROJECT_DISALLOWS_GLOBAL_ACTIVITY)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
|
||||||
$pathStart = 'begin';
|
$pathStart = 'begin';
|
||||||
$pathEnd = 'end';
|
$pathEnd = 'end';
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,14 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not project.globalActivities %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'label.globalActivities'|trans }}</th>
|
||||||
|
<td colspan="3">
|
||||||
|
{{ widgets.label_boolean(project.globalActivities) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% for metaField in project.visibleMetaFields|sort((a, b) => a.order <=> b.order) %}
|
{% for metaField in project.visibleMetaFields|sort((a, b) => a.order <=> b.order) %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ metaField.label|trans }}</th>
|
<th>{{ metaField.label|trans }}</th>
|
||||||
|
|||||||
@@ -60,12 +60,15 @@
|
|||||||
{{ form_row(form.invoiceText) }}
|
{{ form_row(form.invoiceText) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
{{ form_row(form.visible) }}
|
{{ form_row(form.visible) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4 text-center">
|
||||||
{{ form_row(form.billable) }}
|
{{ form_row(form.billable) }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4 text-right">
|
||||||
|
{{ form_row(form.globalActivities) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.metaFields is defined and form.metaFields is not empty %}
|
{% if form.metaFields is defined and form.metaFields is not empty %}
|
||||||
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
|
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class ProjectTest extends AbstractEntityTest
|
|||||||
self::assertNull($sut->getInvoiceText());
|
self::assertNull($sut->getInvoiceText());
|
||||||
self::assertTrue($sut->isVisible());
|
self::assertTrue($sut->isVisible());
|
||||||
self::assertTrue($sut->isBillable());
|
self::assertTrue($sut->isBillable());
|
||||||
|
self::assertTrue($sut->isGlobalActivities());
|
||||||
self::assertNull($sut->getColor());
|
self::assertNull($sut->getColor());
|
||||||
self::assertFalse($sut->hasColor());
|
self::assertFalse($sut->hasColor());
|
||||||
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||||
@@ -106,6 +107,9 @@ class ProjectTest extends AbstractEntityTest
|
|||||||
self::assertFalse($sut->isVisible());
|
self::assertFalse($sut->isVisible());
|
||||||
$sut->setVisible(true);
|
$sut->setVisible(true);
|
||||||
self::assertTrue($sut->isVisible());
|
self::assertTrue($sut->isVisible());
|
||||||
|
|
||||||
|
$sut->setGlobalActivities(false);
|
||||||
|
self::assertFalse($sut->isGlobalActivities());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMetaFields()
|
public function testMetaFields()
|
||||||
|
|||||||
@@ -1201,6 +1201,10 @@
|
|||||||
<source>label.globalsOnly</source>
|
<source>label.globalsOnly</source>
|
||||||
<target>Nur globale</target>
|
<target>Nur globale</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="G9hBaDJ" resname="label.globalActivities">
|
||||||
|
<source>label.globalActivities</source>
|
||||||
|
<target>Globale Tätigkeiten erlauben</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="2Ek7sXu" resname="label.batch_meta_fields">
|
<trans-unit id="2Ek7sXu" resname="label.batch_meta_fields">
|
||||||
<source>label.batch_meta_fields</source>
|
<source>label.batch_meta_fields</source>
|
||||||
<target>Zusätzliche Felder</target>
|
<target>Zusätzliche Felder</target>
|
||||||
@@ -1221,7 +1225,7 @@
|
|||||||
<source>label.includeBudgetType_month</source>
|
<source>label.includeBudgetType_month</source>
|
||||||
<target>Einträge mit „Monats-Budget“ anzeigen</target>
|
<target>Einträge mit „Monats-Budget“ anzeigen</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Wq.h4adsfsdfnD" resname="label.budgetIndependent">
|
<trans-unit id="8mTf0eZ" resname="label.budgetIndependent">
|
||||||
<source>label.budgetIndependent</source>
|
<source>label.budgetIndependent</source>
|
||||||
<target>Unabhängig vom „Budget-Typ“ anzeigen</target>
|
<target>Unabhängig vom „Budget-Typ“ anzeigen</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|||||||
@@ -1201,6 +1201,10 @@
|
|||||||
<source>label.globalsOnly</source>
|
<source>label.globalsOnly</source>
|
||||||
<target>Only global</target>
|
<target>Only global</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="G9hBaDJ" resname="label.globalActivities">
|
||||||
|
<source>label.globalActivities</source>
|
||||||
|
<target>Allow global activities</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="2Ek7sXu" resname="label.batch_meta_fields">
|
<trans-unit id="2Ek7sXu" resname="label.batch_meta_fields">
|
||||||
<source>label.batch_meta_fields</source>
|
<source>label.batch_meta_fields</source>
|
||||||
<target>Additional fields</target>
|
<target>Additional fields</target>
|
||||||
@@ -1221,7 +1225,7 @@
|
|||||||
<source>label.includeBudgetType_month</source>
|
<source>label.includeBudgetType_month</source>
|
||||||
<target>Show entries with "monthly" budget</target>
|
<target>Show entries with "monthly" budget</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Wq.h4adsfsdfnD" resname="label.budgetIndependent">
|
<trans-unit id="8mTf0eZ" resname="label.budgetIndependent">
|
||||||
<source>label.budgetIndependent</source>
|
<source>label.budgetIndependent</source>
|
||||||
<target>Show regardless of "budget-type"</target>
|
<target>Show regardless of "budget-type"</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|||||||
@@ -86,6 +86,10 @@
|
|||||||
<source>This timesheet is already exported.</source>
|
<source>This timesheet is already exported.</source>
|
||||||
<target>Dieser Eintrag wurde bereits exportiert.</target>
|
<target>Dieser Eintrag wurde bereits exportiert.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="mjlHwer0la" resname="Global activities are forbidden for the selected project.">
|
||||||
|
<source>Global activities are forbidden for the selected project.</source>
|
||||||
|
<target>Globale Aktivitäten sind in dem gewählten Projekt nicht erlaubt.</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user