diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml index ebc3372..58b9096 100644 --- a/.github/workflows/linting.yaml +++ b/.github/workflows/linting.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.2', '7.3', '7.4'] + php: ['7.4', '8.0', '8.1'] name: Linting - PHP ${{ matrix.php }} steps: @@ -20,6 +20,6 @@ jobs: coverage: none extensions: intl - run: composer install --no-progress - - run: composer validate + - run: composer validate --strict --no-check-version - run: composer codestyle-check - run: composer phpstan diff --git a/.gitignore b/.gitignore index 661514d..6b75680 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -.gitignore .idea -.php_cs.cache +.disabled +/vendor/ +.php-cs-fixer.cache composer.lock -vendor/ diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 88% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 80e5422..4a64582 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -1,14 +1,15 @@ setRiskyAllowed(true) ->setRules([ 'encoding' => true, @@ -20,7 +21,7 @@ 'function_declaration' => true, 'indentation_type' => true, 'line_ending' => true, - 'lowercase_constants' => true, + 'constant_case' => ['case' => 'lower'], 'lowercase_keywords' => true, 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'], @@ -47,7 +48,7 @@ 'statements' => ['return'], ], 'cast_spaces' => true, - 'class_attributes_separation' => ['elements' => ['method']], + 'class_attributes_separation' => ['elements' => ['method' => 'one']], 'concat_space' => ['spacing' => 'one'], 'declare_equal_normalize' => true, 'function_typehint_space' => true, @@ -101,9 +102,10 @@ ], 'phpdoc_annotation_without_dot' => true, 'phpdoc_indent' => true, - 'phpdoc_inline_tag' => true, + 'phpdoc_inline_tag_normalizer' => true, 'phpdoc_no_access' => true, 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => false, 'phpdoc_no_package' => true, 'phpdoc_no_useless_inheritdoc' => true, 'phpdoc_return_self_reference' => true, @@ -130,7 +132,7 @@ 'standardize_increment' => true, 'standardize_not_equals' => true, 'ternary_operator_spaces' => true, - 'trailing_comma_in_multiline_array' => false, + 'trailing_comma_in_multiline' => false, 'trim_array_spaces' => true, 'unary_operator_spaces' => true, 'whitespace_after_comma_in_array' => true, @@ -141,6 +143,18 @@ 'method', 'property', ]], + 'native_function_invocation' => [ + 'include' => [ + '@compiler_optimized' + ], + 'scope' => 'namespaced' + ], + 'native_function_type_declaration_casing' => true, + 'no_alias_functions' => [ + 'sets' => [ + '@internal' + ] + ], ]) ->setFinder( PhpCsFixer\Finder::create() @@ -149,7 +163,10 @@ ])->exclude([ __DIR__ . '/Resources/', __DIR__ . '/vendor/', + __DIR__ . '/.github/', ]) ) ->setFormat('checkstyle') ; + +return $fixer; diff --git a/CHANGELOG.md b/CHANGELOG.md index d6410c9..6b5729b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.0 + +Compatible with Kimai 1.20.1 + +- Added "default" mode to recalculate ONLY if one of the fields is changed: customer, project, activity, user, price +- Select recalculate mode: off, default, always + ## 0.2 - Added GitHub actions for basic CI diff --git a/DependencyInjection/RecalculateRatesExtension.php b/DependencyInjection/RecalculateRatesExtension.php index 8fb6409..e6d8710 100644 --- a/DependencyInjection/RecalculateRatesExtension.php +++ b/DependencyInjection/RecalculateRatesExtension.php @@ -1,7 +1,7 @@ > + */ + public static function getSubscribedEvents(): array + { + return [ + SystemConfigurationEvent::class => ['onSystemConfiguration', 100], + ]; + } + + public function onSystemConfiguration(SystemConfigurationEvent $event): void + { + $newConfiguration = (new Configuration()) + ->setName('timesheet.recalculate.mode') + ->setLabel('recalculate.mode') + ->setRequired(true) + ->setTranslationDomain('system-configuration') + ->setType(RecalculateModeType::class); + + foreach ($event->getConfigurations() as $configuration) { + if ($configuration->getSection() === 'timesheet') { + $configuration->addConfiguration($newConfiguration); + + return; + } + } + + $event->addConfiguration( + (new SystemConfiguration('recalculate'))->setConfiguration([$newConfiguration]) + ); + } +} diff --git a/Form/RecalculateModeType.php b/Form/RecalculateModeType.php new file mode 100644 index 0000000..ec11aea --- /dev/null +++ b/Form/RecalculateModeType.php @@ -0,0 +1,43 @@ + null]; + + /* @phpstan-ignore-next-line */ + if (\defined('App\Constants::VERSION_ID') && Constants::VERSION_ID > 12000) { + $modes['recalculate.mode.default'] = 'default'; + } + + $modes['recalculate.mode.always'] = 'always'; + + $resolver->setDefaults([ + 'required' => true, + 'multiple' => false, + 'label' => 'kiosk.login_type', + 'choices' => $modes, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/README.md b/README.md index 11b2c79..2e55579 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,54 @@ -# RecalculateRatesBundle +# Recalculate-Rates plugin for Kimai -A Kimai 2 plugin, which forces a recalculation of the hourly and fixed rates for timesheet records on every update. +A Kimai plugin, which forces a recalculation of the hourly and fixed rates for timesheet records on certain updates. -By default, Kimai will use the hourly/fixed rate which was initially found. +There are two possible modes in which this plugin can work: -## Warning +1. Recalculate prices if certain fields were changed +2. Recalculate prices on every update -The good part is: you can change customer/project and activity and be sure, that the correct rate is used. +The first mode is the better one, but only available from Kimai 1.20.1 on. -The bad part: a manually entered hourly/fixed rate will be overwritten. You HAVE to work with the pre-configured rates on your activities/projects/customers. +You can configure the mode, by default mode 1 is used, unless your Kimai version is too old, then 2 is used. + +## Recalculate prices if certain fields were changed + +This mode should be preferred. + +A timesheet record rate will be recalculated if it was changed in one of these fields: Customer, Project, Activity, User, Price + +This still might overwrite custom rates, which were applied to single timesheets. +But this case is very rare and using custom rates for single entries should be avoided anyway. +If you find yourself using this workflow often, consider using the [Expense plugin](https://www.kimai.org/store/expenses-bundle.html). + +## Recalculate prices on every update + +The good part is: +- you can change customer/project and activity and be sure, that the correct rate is used. + +The bad part: +- even setting the export field might change the hourly rate and render your history invalid +- a manually entered hourly/fixed rate will be overwritten +- you HAVE to work with the pre-configured rates on your activities/projects/customers. ## Installation First clone it to your Kimai installation `plugins` directory: -``` -cd /kimai/var/plugins/ +```bash +cd var/plugins/ git clone https://github.com/Keleo/RecalculateRatesBundle.git ``` -And then rebuild the cache: -``` -cd /kimai/ -bin/console cache:clear -bin/console cache:warmup -``` - -You could also [download it as zip](https://github.com/keleo/RecalculateRatesBundle/archive/master.zip) and upload the directory via FTP: +The file structure needs to look like this afterwards: -``` -/kimai/var/plugins/ +```bash +var/plugins/ ├── RecalculateRatesBundle -│   ├── RecalculateRatesBundle.php +│ ├── RecalculateRatesBundle.php | └ ... more files and directories follow here ... ``` + +And then rebuild the cache: +```bash +bin/console kimai:reload --env=prod +``` diff --git a/RecalculateRatesBundle.php b/RecalculateRatesBundle.php index ada895d..e32a4b1 100644 --- a/RecalculateRatesBundle.php +++ b/RecalculateRatesBundle.php @@ -1,7 +1,7 @@ + + + + + label.recalculate + Preisneuberechnung + + + label.recalculate.mode + Modus für Preisneuberechnung + + + recalculate.mode.off + Aus + + + recalculate.mode.always + Bei jeder Änderung + + + recalculate.mode.default + Bei Änderung von Kunde, Projekt, Tätigkeit, Benutzer oder Preis + + + + diff --git a/Resources/translations/system-configuration.en.xlf b/Resources/translations/system-configuration.en.xlf new file mode 100644 index 0000000..a35f56f --- /dev/null +++ b/Resources/translations/system-configuration.en.xlf @@ -0,0 +1,27 @@ + + + + + + label.recalculate + Price recalculation + + + label.recalculate.mode + Mode for price recalculation + + + recalculate.mode.off + Off + + + recalculate.mode.always + With every update + + + recalculate.mode.default + With update of customer, project, activity, user or price + + + + diff --git a/Timesheet/Calculator/RecalculateRateCalculator.php b/Timesheet/Calculator/RecalculateRateCalculator.php index 53f43bb..cbb213f 100644 --- a/Timesheet/Calculator/RecalculateRateCalculator.php +++ b/Timesheet/Calculator/RecalculateRateCalculator.php @@ -1,7 +1,7 @@ calculator = $calculator; + $this->systemConfiguration = $systemConfiguration; } /** @@ -29,19 +34,70 @@ public function __construct(RateCalculator $calculator) */ public function calculate(Timesheet $record) { - if (null !== $record->getHourlyRate()) { - $record->setHourlyRate(null); + $mode = $this->systemConfiguration->find('timesheet.recalculate.mode'); + if ($mode === null) { + return; } - if (null !== $record->getFixedRate()) { - $record->setFixedRate(null); + // this means we have Kimai > 1.20 + // we can use the improved calculation method + if ($mode === 'default' && \func_num_args() === 2) { + /** @var array $changes */ + $changes = func_get_arg(1); + + // check if the rate was changed manually + $changedRate = false; + foreach (['hourlyRate', 'fixedRate', 'internalRate', 'rate'] as $field) { + if (\array_key_exists($field, $changes)) { + $changedRate = true; + break; + } + } + + // if no manual rate changed was applied: + // check if a field changed, that is relevant for the rate calculation: if one was changed => + // reset all rates, because most users do not even see their rates and would not be able + // to fix or empty the rate, even if they knew that the changed project has another base rate + if (!$changedRate) { + foreach (['project', 'activity', 'user'] as $field) { + if (\array_key_exists($field, $changes)) { + // this has room for minor improvements: entries with a manual rate might be changed + $this->resetRates($record); + break; + } + } + } + + return; + } + + if ($mode === 'always') { + $this->resetRates($record); + + // we have to trigger it again, as there was no order defined for the calculator in earlier versions + /* @phpstan-ignore-next-line */ + if (\defined('App\Constants::VERSION_ID') && Constants::VERSION_ID < 12001) { + $this->calculator->calculate($record); + } } + } + + private function resetRates(Timesheet $record): void + { + if (method_exists($record, 'resetRates')) { + $record->resetRates(); - if (null !== $record->getInternalRate()) { - $record->setInternalRate(null); + return; } - // we have to trigger it again, as there is no order defined for the calculator - $this->calculator->calculate($record); + $record->setRate(0.00); + $record->setInternalRate(null); + $record->setHourlyRate(null); + $record->setFixedRate(null); + } + + public function getPriority(): int + { + return 250; } } diff --git a/composer.json b/composer.json index bc52ea6..0026138 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,9 @@ { "name": "keleo/recalculate-rates-bundle", - "description": "A Kimai 2 plugin, which forces to recalculate the hourly/fixed rate on every timesheet update.", + "description": "A Kimai plugin, which forces recalculation of hourly/fixed rates on timesheet update.", "homepage": "https://www.kimai.org/store/keleo-recalculate-rates-bundle.html", "type": "kimai-plugin", - "version": "0.2", + "version": "1.0", "keywords": [ "kimai", "kimai-plugin" @@ -12,18 +12,29 @@ "authors": [ { "name": "Kevin Papst", - "email": "kpapst@gmx.net", + "email": "kevin@kevinpapst.de", "homepage": "https://www.keleo.de" } ], "extra": { "kimai": { - "require": "0.9", - "version": "0.2", + "require": "1.20.1", "name": "RecalculateRatesBundle" } }, + "autoload": { + "psr-4": { + "KimaiPlugin\\InvoiceBundle\\": "" + } + }, "config": { + "allow-plugins": { + "composer/package-versions-deprecated": false, + "symfony/flex": false + }, + "platform": { + "php": "7.3" + }, "preferred-install": { "*": "dist" }, @@ -32,19 +43,19 @@ "scripts": { "codestyle": "vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=none", "codestyle-fix": "vendor/bin/php-cs-fixer fix", - "codestyle-check": "vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php_cs.dist --using-cache=no --show-progress=none --format=checkstyle", - "phpstan": "vendor/bin/phpstan analyse . -c phpstan.neon --level=7", + "codestyle-check": "vendor/bin/php-cs-fixer fix --dry-run --verbose --using-cache=no --show-progress=none --format=checkstyle", + "phpstan": "vendor/bin/phpstan analyse . -c phpstan.neon --level=9", "linting": [ - "composer validate", + "composer validate --strict --no-check-version", "@codestyle-check", "@phpstan" ] }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.15", - "kevinpapst/kimai2": ">1.9", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-symfony": "^0.12", + "friendsofphp/php-cs-fixer": "^3.0", + "kevinpapst/kimai2": "^1.20", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-symfony": "^1.0", "symfony/console": "^4.0", "symfony/event-dispatcher": "^4.0" } diff --git a/phpstan.neon b/phpstan.neon index 7e31e59..05226fb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,9 @@ includes: - - vendor/phpstan/phpstan-symfony/extension.neon - - vendor/phpstan/phpstan-symfony/rules.neon + - %rootDir%/../phpstan-symfony/extension.neon + - %rootDir%/../phpstan-symfony/rules.neon parameters: - excludes_analyse: + excludePaths: - vendor/ treatPhpDocTypesAsCertain: false inferPrivatePropertyTypeFromConstructor: true