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)
|
||||
*/
|
||||
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()
|
||||
{
|
||||
@@ -419,6 +428,16 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isGlobalActivities(): bool
|
||||
{
|
||||
return $this->globalActivities;
|
||||
}
|
||||
|
||||
public function setGlobalActivities(bool $globalActivities): void
|
||||
{
|
||||
$this->globalActivities = $globalActivities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\DateTimePickerType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -95,7 +96,11 @@ class ProjectEditForm extends AbstractType
|
||||
'placeholder' => (null === $id && null === $customer) ? '' : false,
|
||||
'customers' => $customer,
|
||||
'query_builder_for_user' => true,
|
||||
]);
|
||||
])
|
||||
->add('globalActivities', YesNoType::class, [
|
||||
'label' => 'label.globalActivities',
|
||||
])
|
||||
;
|
||||
|
||||
$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()) {
|
||||
$mainQuery->add($qb->expr()->isNull('a.project'));
|
||||
} elseif ($query->hasProjects()) {
|
||||
$mainQuery->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('a.project'),
|
||||
$orX = $qb->expr()->orX(
|
||||
$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());
|
||||
}
|
||||
|
||||
@@ -381,8 +397,22 @@ class ActivityRepository extends EntityRepository
|
||||
);
|
||||
|
||||
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'));
|
||||
}
|
||||
}
|
||||
|
||||
$where->add($orX);
|
||||
$qb->setParameter('project', $query->getProjects());
|
||||
|
||||
@@ -27,6 +27,7 @@ class TimesheetBasic extends TimesheetConstraint
|
||||
public const DISABLED_CUSTOMER_ERROR = 'kimai-timesheet-89';
|
||||
public const PROJECT_NOT_STARTED = 'kimai-timesheet-91';
|
||||
public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92';
|
||||
public const PROJECT_DISALLOWS_GLOBAL_ACTIVITY = 'kimai-timesheet-93';
|
||||
|
||||
protected static $errorNames = [
|
||||
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::PROJECT_NOT_STARTED => 'The project has not started 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.';
|
||||
|
||||
@@ -124,6 +124,14 @@ final class TimesheetBasicValidator extends ConstraintValidator
|
||||
->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';
|
||||
$pathEnd = 'end';
|
||||
|
||||
|
||||
@@ -116,6 +116,14 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% 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) %}
|
||||
<tr>
|
||||
<th>{{ metaField.label|trans }}</th>
|
||||
|
||||
@@ -60,12 +60,15 @@
|
||||
{{ form_row(form.invoiceText) }}
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.visible) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4 text-center">
|
||||
{{ form_row(form.billable) }}
|
||||
</div>
|
||||
<div class="col-md-4 text-right">
|
||||
{{ form_row(form.globalActivities) }}
|
||||
</div>
|
||||
</div>
|
||||
{% 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) %}
|
||||
|
||||
@@ -38,6 +38,7 @@ class ProjectTest extends AbstractEntityTest
|
||||
self::assertNull($sut->getInvoiceText());
|
||||
self::assertTrue($sut->isVisible());
|
||||
self::assertTrue($sut->isBillable());
|
||||
self::assertTrue($sut->isGlobalActivities());
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertFalse($sut->hasColor());
|
||||
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
@@ -106,6 +107,9 @@ class ProjectTest extends AbstractEntityTest
|
||||
self::assertFalse($sut->isVisible());
|
||||
$sut->setVisible(true);
|
||||
self::assertTrue($sut->isVisible());
|
||||
|
||||
$sut->setGlobalActivities(false);
|
||||
self::assertFalse($sut->isGlobalActivities());
|
||||
}
|
||||
|
||||
public function testMetaFields()
|
||||
|
||||
@@ -1201,6 +1201,10 @@
|
||||
<source>label.globalsOnly</source>
|
||||
<target>Nur globale</target>
|
||||
</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">
|
||||
<source>label.batch_meta_fields</source>
|
||||
<target>Zusätzliche Felder</target>
|
||||
@@ -1221,7 +1225,7 @@
|
||||
<source>label.includeBudgetType_month</source>
|
||||
<target>Einträge mit „Monats-Budget“ anzeigen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wq.h4adsfsdfnD" resname="label.budgetIndependent">
|
||||
<trans-unit id="8mTf0eZ" resname="label.budgetIndependent">
|
||||
<source>label.budgetIndependent</source>
|
||||
<target>Unabhängig vom „Budget-Typ“ anzeigen</target>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1201,6 +1201,10 @@
|
||||
<source>label.globalsOnly</source>
|
||||
<target>Only global</target>
|
||||
</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">
|
||||
<source>label.batch_meta_fields</source>
|
||||
<target>Additional fields</target>
|
||||
@@ -1221,7 +1225,7 @@
|
||||
<source>label.includeBudgetType_month</source>
|
||||
<target>Show entries with "monthly" budget</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wq.h4adsfsdfnD" resname="label.budgetIndependent">
|
||||
<trans-unit id="8mTf0eZ" resname="label.budgetIndependent">
|
||||
<source>label.budgetIndependent</source>
|
||||
<target>Show regardless of "budget-type"</target>
|
||||
</trans-unit>
|
||||
|
||||
@@ -86,6 +86,10 @@
|
||||
<source>This timesheet is already exported.</source>
|
||||
<target>Dieser Eintrag wurde bereits exportiert.</target>
|
||||
</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>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Reference in New Issue
Block a user