* added unit tests and documentation #112 * fixed DashboardController * added CONTRIBUTION guidelines * fixed license year
This commit is contained in:
@@ -118,7 +118,11 @@ class KimaiImporterCommand extends Command
|
||||
->setName('kimai:import-v1')
|
||||
->setDescription('Import data from a Kimai v1 installation')
|
||||
->setHelp('This command allows you to import the most important data from a Kimi v1 installation.')
|
||||
->addArgument('connection', InputArgument::REQUIRED, 'The database connection as URL, for example: mysql://user:password@127.0.0.1:3306/kimai?charset=latin1')
|
||||
->addArgument(
|
||||
'connection',
|
||||
InputArgument::REQUIRED,
|
||||
'The database connection as URL, e.g.: mysql://user:password@127.0.0.1:3306/kimai?charset=latin1'
|
||||
)
|
||||
->addArgument('prefix', InputArgument::REQUIRED, 'The database prefix for the old Kimai v1 tables')
|
||||
->addArgument('password', InputArgument::REQUIRED, 'The new password for all imported user')
|
||||
->addArgument('country', InputArgument::OPTIONAL, 'The default country for customer', 'de')
|
||||
@@ -272,7 +276,8 @@ class KimaiImporterCommand extends Command
|
||||
'Start: ' . $this->bytesHumanReadable($bytesStart) . PHP_EOL .
|
||||
'After caching: ' . $this->bytesHumanReadable($bytesCached) . PHP_EOL .
|
||||
'After import: ' . $this->bytesHumanReadable($bytesImported) . PHP_EOL .
|
||||
'Total consumption for importing '.$allImports.' new database entries: ' . $this->bytesHumanReadable($bytesImported - $bytesStart)
|
||||
'Total consumption for importing '.$allImports.' new database entries: ' .
|
||||
$this->bytesHumanReadable($bytesImported - $bytesStart)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,8 +293,11 @@ class KimaiImporterCommand extends Command
|
||||
*/
|
||||
protected function checkDatabaseVersion(SymfonyStyle $io, $requiredVersion, $requiredRevision)
|
||||
{
|
||||
$version = $this->getImportConnection()->query('SELECT value from ' . $this->dbPrefix . 'configuration WHERE option = "version"')->fetchColumn();
|
||||
$revision = $this->getImportConnection()->query('SELECT value from ' . $this->dbPrefix . 'configuration WHERE option = "revision"')->fetchColumn();
|
||||
$versionQuery = 'SELECT value from ' . $this->dbPrefix . 'configuration WHERE option = "version"';
|
||||
$revisionQuery = 'SELECT value from ' . $this->dbPrefix . 'configuration WHERE option = "revision"';
|
||||
|
||||
$version = $this->getImportConnection()->query($versionQuery)->fetchColumn();
|
||||
$revision = $this->getImportConnection()->query($revisionQuery)->fetchColumn();
|
||||
|
||||
if (version_compare($requiredVersion, $version) == 1) {
|
||||
$io->error(
|
||||
@@ -645,7 +653,9 @@ class KimaiImporterCommand extends Command
|
||||
$project = null;
|
||||
|
||||
if (!isset($this->projects[$projectId])) {
|
||||
throw new \Exception('Invalid project linked to activity ' . $oldActivity['name'] . ': ' . $projectId);
|
||||
throw new \Exception(
|
||||
'Invalid project linked to activity ' . $oldActivity['name'] . ': ' . $projectId
|
||||
);
|
||||
}
|
||||
|
||||
$project = $this->projects[$projectId];
|
||||
@@ -668,8 +678,12 @@ class KimaiImporterCommand extends Command
|
||||
* @return Activity
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function createActivity(SymfonyStyle $io, ObjectManager $entityManager, Project $project, array $oldActivity)
|
||||
{
|
||||
protected function createActivity(
|
||||
SymfonyStyle $io,
|
||||
ObjectManager $entityManager,
|
||||
Project $project,
|
||||
array $oldActivity
|
||||
) {
|
||||
$activityId = $oldActivity['activityID'];
|
||||
if (isset($this->activities[$activityId][$project->getId()])) {
|
||||
return $this->activities[$activityId][$project->getId()];
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
@@ -104,7 +105,7 @@ class DashboardController extends Controller
|
||||
//"{{ widgets.info_box_counter('stats.amountThisMonth', timesheetGlobal.amountThisMonth|money, 'money', 'green') }}",
|
||||
"{{ widgets.info_box_counter('stats.durationTotal', timesheetGlobal.durationTotal|duration(true), 'hourglass-o', 'yellow') }}",
|
||||
//"{{ widgets.info_box_counter('stats.amountTotal', timesheetGlobal.amountTotal|money, 'money', 'red') }}",
|
||||
"{{ widgets.info_box_counter('stats.activeRecordings', timesheetGlobal.activeCurrently, 'hourglass-o', 'red', path('admin_timesheet', {'state': 1})) }}",
|
||||
"{{ widgets.info_box_counter('stats.activeRecordings', timesheetGlobal.activeCurrently, 'hourglass-o', 'red', path('admin_timesheet', {'state': ".TimesheetQuery::STATE_RUNNING."})) }}",
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -88,15 +88,32 @@ class AppFixtures extends Fixture
|
||||
protected function getUserDefinition()
|
||||
{
|
||||
return [
|
||||
['Clara Haynes', 'CFO', 'clara_customer', 'clara_customer@example.com', 'ROLE_CUSTOMER', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y', true],
|
||||
['John Doe', 'Developer', 'john_user', 'john_user@example.com', 'ROLE_USER', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', true],
|
||||
[
|
||||
'Clara Haynes', 'CFO', 'clara_customer', 'clara_customer@example.com', 'ROLE_CUSTOMER',
|
||||
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y', true
|
||||
],
|
||||
[
|
||||
'John Doe', 'Developer', 'john_user', 'john_user@example.com', 'ROLE_USER',
|
||||
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', true
|
||||
],
|
||||
// inactive user to test login
|
||||
['Chris Deactive', 'Developer (left company)', 'chris_user', 'chris_user@example.com', 'ROLE_USER', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', false],
|
||||
['Tony Maier', 'Head of Development', 'tony_teamlead', 'tony_teamlead@example.com', 'ROLE_TEAMLEAD', 'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg', true],
|
||||
[
|
||||
'Chris Deactive', 'Developer (left company)', 'chris_user', 'chris_user@example.com', 'ROLE_USER',
|
||||
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', false
|
||||
],
|
||||
[
|
||||
'Tony Maier', 'Head of Development', 'tony_teamlead', 'tony_teamlead@example.com', 'ROLE_TEAMLEAD',
|
||||
'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg', true
|
||||
],
|
||||
// no avatar to test default image macro
|
||||
['Anna Smith', 'Administrator', 'anna_admin', 'anna_admin@example.com', 'ROLE_ADMIN', null, true],
|
||||
[
|
||||
'Anna Smith', 'Administrator', 'anna_admin', 'anna_admin@example.com', 'ROLE_ADMIN', null, true
|
||||
],
|
||||
// no alias to test twig username macro
|
||||
[null, 'Super Administrator', 'susan_super', 'susan_super@example.com', 'ROLE_SUPER_ADMIN', '/bundles/avanzuadmintheme/img/avatar.png', true]
|
||||
[
|
||||
null, 'Super Administrator', 'susan_super', 'susan_super@example.com', 'ROLE_SUPER_ADMIN',
|
||||
'/bundles/avanzuadmintheme/img/avatar.png', true
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,14 +44,16 @@ class InvoiceFixtures extends Fixture
|
||||
->setVat(19)
|
||||
->setDueDays(14)
|
||||
->setPaymentTerms(
|
||||
'I would like to thank you for your confidence and will gladly be there for you in the future.
|
||||
Please transfer the total amount within 14 days to the given account and use the invoice number as reference.'
|
||||
'I would like to thank you for your confidence and will gladly be there for you in the future.' .
|
||||
PHP_EOL .
|
||||
'Please transfer the total amount within 14 days to the given account and use the invoice number ' .
|
||||
'as reference.'
|
||||
)
|
||||
->setAddress(
|
||||
'795 Folsom Ave, Suite 600
|
||||
San Francisco, CA 94107
|
||||
Phone: (804) 123-5432
|
||||
Email: info@almasaeedstudio.com'
|
||||
'795 Folsom Ave, Suite 600' . PHP_EOL .
|
||||
'San Francisco, CA 94107' . PHP_EOL .
|
||||
'Phone: (804) 123-456789' . PHP_EOL .
|
||||
'Email: email@example.com'
|
||||
)
|
||||
;
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ class AppExtension extends Extension
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.invoice', $config['invoice']);
|
||||
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
|
||||
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,52 @@ class Configuration implements ConfigurationInterface
|
||||
->children()
|
||||
->arrayNode('timesheet')
|
||||
->children()
|
||||
->integerNode('rounding')->end()
|
||||
->arrayNode('rounding')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->integerNode('begin')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->integerNode('end')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->integerNode('duration')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->defaultValue(array())
|
||||
->end()
|
||||
|
||||
->arrayNode('rates')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->floatNode('factor')
|
||||
->isRequired()
|
||||
->defaultValue(1)
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('invoice')
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Doctrine;
|
||||
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
@@ -20,6 +20,27 @@ use App\Entity\Timesheet;
|
||||
*/
|
||||
class TimesheetSubscriber implements EventSubscriber
|
||||
{
|
||||
/**
|
||||
* @var CalculatorInterface[]
|
||||
*/
|
||||
protected $calculator;
|
||||
|
||||
/**
|
||||
* TimesheetSubscriber constructor.
|
||||
* @param iterable $calculators
|
||||
*/
|
||||
public function __construct(iterable $calculators)
|
||||
{
|
||||
foreach ($calculators as $calculator) {
|
||||
if (!($calculator instanceof CalculatorInterface)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid TimesheetCalculator implementation given. Expected CalculatorInterface but received ' .
|
||||
get_class($calculator)
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->calculator = $calculators;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
@@ -55,26 +76,12 @@ class TimesheetSubscriber implements EventSubscriber
|
||||
{
|
||||
$entity = $args->getObject();
|
||||
|
||||
if ($entity instanceof Timesheet) {
|
||||
if ($entity->getEnd() !== null) {
|
||||
$duration = $entity->getEnd()->getTimestamp() - $entity->getBegin()->getTimestamp();
|
||||
$entity->setDuration($duration);
|
||||
if (!($entity instanceof Timesheet)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO allow to set hourly rate on activity, project and customer and prefer these
|
||||
|
||||
$rate = $this->calculateRate($entity);
|
||||
$entity->setRate($rate);
|
||||
}
|
||||
foreach ($this->calculator as $calculator) {
|
||||
$calculator->calculate($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entity
|
||||
* @return float
|
||||
*/
|
||||
protected function calculateRate(Timesheet $entity)
|
||||
{
|
||||
$hourlyRate = (float) $entity->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0);
|
||||
return (float) $hourlyRate * ($entity->getDuration() / 3600);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<?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;
|
||||
|
||||
use App\DependencyInjection\AppExtension;
|
||||
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||
@@ -27,6 +35,11 @@ class Kernel extends BaseKernel
|
||||
return $this->getProjectDir().'/var/log';
|
||||
}
|
||||
|
||||
protected function build(ContainerBuilder $container)
|
||||
{
|
||||
$container->registerForAutoconfiguration(CalculatorInterface::class)->addTag('timesheet.calculator');
|
||||
}
|
||||
|
||||
public function registerBundles()
|
||||
{
|
||||
$contents = require $this->getProjectDir().'/config/bundles.php';
|
||||
|
||||
139
src/Timesheet/Calculator/DurationCalculator.php
Normal file
139
src/Timesheet/Calculator/DurationCalculator.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?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\Timesheet\Calculator;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
|
||||
/**
|
||||
* Implementation to calculate the durations for a timesheet record.
|
||||
*
|
||||
* This calculator takes the configuration %kimai.timesheet.rounding% as argument,
|
||||
* so its rounding behaviour can be customized.
|
||||
*/
|
||||
class DurationCalculator implements CalculatorInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $roundings;
|
||||
|
||||
/**
|
||||
* DurationCalculator constructor.
|
||||
* @param array $roundings
|
||||
*/
|
||||
public function __construct(array $roundings)
|
||||
{
|
||||
$this->roundings = $roundings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
*/
|
||||
public function calculate(Timesheet $record)
|
||||
{
|
||||
if ($record->getEnd() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applyDuration($record);
|
||||
$this->applyRoundings($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
*/
|
||||
protected function applyDuration(Timesheet $record)
|
||||
{
|
||||
$duration = $record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp();
|
||||
$record->setDuration($duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
*/
|
||||
protected function applyRoundings(Timesheet $record)
|
||||
{
|
||||
foreach ($this->roundings as $rounding) {
|
||||
$weekday = $record->getEnd()->format('l');
|
||||
$days = array_map('strtolower', $rounding['days']);
|
||||
|
||||
if (in_array(strtolower($weekday), $days)) {
|
||||
$this->roundBegin($record, $rounding['begin']);
|
||||
$this->roundEnd($record, $rounding['end']);
|
||||
$this->applyDuration($record);
|
||||
$this->roundDuration($record, $rounding['duration']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
protected function roundBegin(Timesheet $record, $minutes)
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = $record->getBegin()->getTimestamp();
|
||||
$seconds = $minutes * 60;
|
||||
$diff = $timestamp % $seconds;
|
||||
|
||||
if ($diff === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$record->getBegin()->setTimestamp($timestamp - $diff);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
protected function roundEnd(Timesheet $record, $minutes)
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = $record->getEnd()->getTimestamp();
|
||||
$seconds = $minutes * 60;
|
||||
$diff = $timestamp % $seconds;
|
||||
|
||||
if ($diff === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$record->getEnd()->setTimestamp($timestamp - $diff + $seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
protected function roundDuration(Timesheet $record, $minutes)
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = $record->getDuration();
|
||||
$seconds = $minutes * 60;
|
||||
$diff = $timestamp % $seconds;
|
||||
|
||||
if ($diff === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$record->setDuration($timestamp - $diff + $seconds);
|
||||
}
|
||||
}
|
||||
86
src/Timesheet/Calculator/RateCalculator.php
Normal file
86
src/Timesheet/Calculator/RateCalculator.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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\Timesheet\Calculator;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
|
||||
/**
|
||||
* Implementation to calculate the rate for a timesheet record.
|
||||
*/
|
||||
class RateCalculator implements CalculatorInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $rates;
|
||||
|
||||
/**
|
||||
* RateCalculator constructor.
|
||||
* @param array $rates
|
||||
*/
|
||||
public function __construct(array $rates)
|
||||
{
|
||||
$this->rates = $rates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
*/
|
||||
public function calculate(Timesheet $record)
|
||||
{
|
||||
if ($record->getEnd() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rate = $this->calculateRate($record);
|
||||
$factor = $this->getRateFactor($record);
|
||||
|
||||
$record->setRate($rate * $factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @return float
|
||||
*/
|
||||
protected function getRateFactor(Timesheet $record)
|
||||
{
|
||||
$factor = 0;
|
||||
foreach ($this->rates as $rateFactor) {
|
||||
$weekday = $record->getEnd()->format('l');
|
||||
$days = array_map('strtolower', $rateFactor['days']);
|
||||
if (in_array(strtolower($weekday), $days)) {
|
||||
if ($rateFactor['factor'] <= 0) {
|
||||
throw new \InvalidArgumentException(
|
||||
'A rate factor smaller or equals 0 is not allowed, given: ' . $rateFactor['factor']
|
||||
);
|
||||
}
|
||||
$factor += $rateFactor['factor'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($factor <= 0) {
|
||||
$factor = 1;
|
||||
}
|
||||
|
||||
return $factor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @return float
|
||||
*/
|
||||
protected function calculateRate(Timesheet $record)
|
||||
{
|
||||
$hourlyRate = (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0);
|
||||
return (float) $hourlyRate * ($record->getDuration() / 3600);
|
||||
}
|
||||
}
|
||||
28
src/Timesheet/CalculatorInterface.php
Normal file
28
src/Timesheet/CalculatorInterface.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\Timesheet;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
/**
|
||||
* A calculator is called before a Timesheet entity will be updated.
|
||||
* These classes will normally be used when calculating duration or rates.
|
||||
*/
|
||||
interface CalculatorInterface
|
||||
{
|
||||
/**
|
||||
* All necessary changes need to be applied on the given $record.
|
||||
* The methods return value will not be evaluated.
|
||||
*
|
||||
* @param Timesheet $record
|
||||
* @return void
|
||||
*/
|
||||
public function calculate(Timesheet $record);
|
||||
}
|
||||
Reference in New Issue
Block a user