Release 1.6.1 (#1282)

This commit is contained in:
Kevin Papst
2019-11-26 17:22:31 +01:00
committed by GitHub
parent 724eb9c658
commit 5ee0396a02
27 changed files with 240 additions and 112 deletions

View File

@@ -1,5 +1,8 @@
# Change Log
## [1.6.1](https://github.com/kevinpapst/kimai2/tree/1.6.1) (2019-11-26)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6...1.6.1)
## [1.6](https://github.com/kevinpapst/kimai2/tree/1.6) (2019-11-24)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.5...1.6)

View File

@@ -8,7 +8,7 @@ you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [1.6](https://github.com/kevinpapst/kimai2/releases/tag/1.6)
## [1.6](https://github.com/kevinpapst/kimai2/releases/tag/1.6) and [1.6.1](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**

View File

@@ -93,7 +93,6 @@ body.invoice_print {
/* table */
table {
table-layout: fixed;
width: 100%;
}
@@ -124,19 +123,14 @@ body.invoice_print {
article address {
font-size: 1em;
float: left;
margin: 1em 0;
}
article.address {
margin-bottom: 1.5em;
margin:1em 0 1.5em 0;
}
/* article */
article, table.meta, table.inventory {
margin: 1em 0;
}
article:after {
clear: both;
content: "";
@@ -182,6 +176,7 @@ body.invoice_print {
table.balance {
float: right;
margin-top: 1em;
th {
text-align: right;
font-weight: normal;
@@ -226,29 +221,6 @@ body.invoice_print {
table.inventory tbody tr:last-child td {
padding-bottom: 15px
}
table.inventory td:nth-child(1),
table.inventory th:nth-child(1) {
text-align: left;
}
table.inventory td:nth-child(2),
table.inventory th:nth-child(2) {
text-align: center;
width: 20%;
}
table.inventory td:nth-child(3),
table.inventory th:nth-child(3) {
text-align: center;
width: 20%;
}
table.inventory td:nth-child(4),
table.inventory th:nth-child(4) {
text-align: right;
width: 20%;
}
.footer {
border-color: #000;

View File

@@ -31,7 +31,7 @@
"mpdf/mpdf": "^7.1",
"nelmio/api-doc-bundle": "^3.2",
"nelmio/cors-bundle": "^1.5",
"ocramius/proxy-manager": "2.1.1",
"ocramius/proxy-manager": "^2.1.1",
"phpoffice/phpspreadsheet": "^1.4",
"phpoffice/phpword": "^0.15.0",
"psr/log": "^1.1",
@@ -124,11 +124,14 @@
"post-update-cmd": [
"@auto-scripts"
],
"kimai:code-check": [
"kimai:pre-commit": [
"@kimai:codestyle",
"@kimai:phpstan",
"@kimai:code-lint",
"@kimai:tests-unit",
"@kimai:tests-unit"
],
"kimai:code-check": [
"@kimai:pre-commit",
"@kimai:tests-integration"
],
"kimai:code-lint": [

33
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "89fb45fe0706c25775a066f5f6138d62",
"content-hash": "4c71764ce6f4c135070a2f7e7a0c62b6",
"packages": [
{
"name": "beberlei/doctrineextensions",
@@ -4006,33 +4006,34 @@
},
{
"name": "ocramius/proxy-manager",
"version": "2.1.1",
"version": "2.2.3",
"source": {
"type": "git",
"url": "https://github.com/Ocramius/ProxyManager.git",
"reference": "e18ac876b2e4819c76349de8f78ccc8ef1554cd7"
"reference": "4d154742e31c35137d5374c998e8f86b54db2e2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Ocramius/ProxyManager/zipball/e18ac876b2e4819c76349de8f78ccc8ef1554cd7",
"reference": "e18ac876b2e4819c76349de8f78ccc8ef1554cd7",
"url": "https://api.github.com/repos/Ocramius/ProxyManager/zipball/4d154742e31c35137d5374c998e8f86b54db2e2f",
"reference": "4d154742e31c35137d5374c998e8f86b54db2e2f",
"shasum": ""
},
"require": {
"ocramius/package-versions": "^1.1.1",
"php": "^7.1.0",
"zendframework/zend-code": "^3.1.0"
"ocramius/package-versions": "^1.1.3",
"php": "^7.2.0",
"zendframework/zend-code": "^3.3.0"
},
"require-dev": {
"couscous/couscous": "^1.5.2",
"couscous/couscous": "^1.6.1",
"ext-phar": "*",
"humbug/humbug": "dev-master@DEV",
"nikic/php-parser": "^3.0.4",
"humbug/humbug": "1.0.0-RC.0@RC",
"nikic/php-parser": "^3.1.1",
"padraic/phpunit-accelerator": "dev-master@DEV",
"phpbench/phpbench": "^0.12.2",
"phpstan/phpstan": "^0.6.4",
"phpunit/phpunit": "^5.6.4",
"phpunit/phpunit-mock-objects": "^3.4.1",
"squizlabs/php_codesniffer": "^2.7.0"
"phpstan/phpstan": "dev-master#856eb10a81c1d27c701a83f167dc870fd8f4236a as 0.9.999",
"phpstan/phpstan-phpunit": "dev-master#5629c0a1f4a9c417cb1077cf6693ad9753895761",
"phpunit/phpunit": "^6.4.3",
"squizlabs/php_codesniffer": "^2.9.1"
},
"suggest": {
"ocramius/generated-hydrator": "To have very fast object to array to object conversion for ghost objects",
@@ -4071,7 +4072,7 @@
"proxy pattern",
"service proxies"
],
"time": "2017-05-04T11:12:50+00:00"
"time": "2019-08-10T08:37:15+00:00"
},
{
"name": "pagerfanta/pagerfanta",

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@
"build/app.926c5dcb.js"
],
"css": [
"build/app.fe1cac78.css"
"build/app.b2938041.css"
]
},
"chart": {
@@ -36,7 +36,7 @@
"build/0.1a6ffb43.js": "sha384-vuVCyLVj2b4h3jpKl+JSANRCacmXeLk+ihK5T5NZBO7+SJ5Y+o6d4qju116hTPSD",
"build/1.16767f92.js": "sha384-JPoKdrVtBemSiVBoAnmSxLML7xXM9zYeuwOPYQv/kLzt/P4cmLY5r9gH8oaGRPFG",
"build/app.926c5dcb.js": "sha384-DENClAG7XobiwMAnu1d/3xKCmR/7jYHN7/WIf5xdO6VUmYoHdm3QW2cOvxh97nnd",
"build/app.fe1cac78.css": "sha384-Z5aYp8LzFi0TRFbEFhM2SJzVMmesVfuSaDD66Z7vCkpuCO7+TJB+0xXrYdQqwsSF",
"build/app.b2938041.css": "sha384-i4bGiyQZqNVMXkq172uvWaq/m9cIzAkao9b1zbOpESXWGPkOmkTPwSGnqiSJVNAp",
"build/2.dad56560.js": "sha384-oKsefSmRq0GL+Qa6f6jExXvFay3cM6YZFHQRKoAHi2p54UOCtjJfjF2eAB1qykoL",
"build/chart.7f4d7f29.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
"build/calendar.bd4fe3ae.js": "sha384-YRVsW05mn+SMT/9LyfRafnVOGV6c6MN6eKjIX0gcq2BWl97HrieuWZgeR0g8hPtU",

View File

@@ -2,7 +2,7 @@
"build/0.1a6ffb43.js": "build/0.1a6ffb43.js",
"build/1.16767f92.js": "build/1.16767f92.js",
"build/2.dad56560.js": "build/2.dad56560.js",
"build/app.css": "build/app.fe1cac78.css",
"build/app.css": "build/app.b2938041.css",
"build/app.js": "build/app.926c5dcb.js",
"build/calendar.css": "build/calendar.ade7bcdf.css",
"build/calendar.js": "build/calendar.bd4fe3ae.js",

View File

@@ -42,7 +42,8 @@ class ImportTimesheetCommand extends Command
{
protected static $defaultName = 'kimai:import:timesheet';
public const DEFAULT_BEGIN = '00:00';
// if we use 00:00 we might run into summer/winter time problems which happen between 02:00 and 03:00
public const DEFAULT_BEGIN = '04:00';
public const DEFAULT_CUSTOMER = 'Imported customer - %s';
private static $supportedHeader = [

View File

@@ -136,12 +136,6 @@ final class InstallCommand extends Command
protected function createDatabase(SymfonyStyle $io, InputInterface $input, OutputInterface $output)
{
if (!$this->connection->isConnected() && !$this->connection->connect()) {
throw new \Exception(
sprintf('Database connection could not be established: %s', $this->connection->getDatabase())
);
}
if ($this->connection->isConnected()) {
$io->note(sprintf('Database is existing and connection could be established'));
@@ -153,7 +147,11 @@ final class InstallCommand extends Command
}
$command = $this->getApplication()->find('doctrine:database:create');
$command->run(new ArrayInput([]), $output);
$result = $command->run(new ArrayInput(['--if-not-exists' => true]), $output);
if (0 !== $result) {
throw new \Exception('Failed creating database. Check your credentials in DATABASE_URL');
}
}
/**

View File

@@ -17,7 +17,7 @@ class Constants
/**
* The current release version
*/
public const VERSION = '1.6';
public const VERSION = '1.6.1';
/**
* The current release status, either "stable" or "dev"
*/

View File

@@ -287,6 +287,9 @@ class InvoiceController extends AbstractController
->setPaymentTerms($copyFrom->getPaymentTerms())
->setAddress($copyFrom->getAddress())
->setNumberGenerator($copyFrom->getNumberGenerator())
->setContact($copyFrom->getContact())
->setPaymentDetails($copyFrom->getPaymentDetails())
->setVatId($copyFrom->getVatId())
;
}

View File

@@ -12,6 +12,7 @@ namespace App\Invoice\Calculator;
use App\Entity\Timesheet;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceItemInterface;
use App\Invoice\InvoiceItemWithAmountInterface;
abstract class AbstractMergedCalculator extends AbstractCalculator
{
@@ -32,7 +33,11 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
$duration += $entry->getDuration();
}
$invoiceItem->setAmount($invoiceItem->getAmount() + 1);
$amount = 1;
if ($entry instanceof InvoiceItemWithAmountInterface) {
$amount = $entry->getAmount();
}
$invoiceItem->setAmount($invoiceItem->getAmount() + $amount);
$invoiceItem->setUser($entry->getUser());
$invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate());
$invoiceItem->setDuration($duration);

View File

@@ -45,8 +45,8 @@ class ShortInvoiceCalculator extends AbstractMergedCalculator implements Calcula
if (count($keys) > 1) {
$invoiceItem->setAmount(1);
$invoiceItem->setFixedRate($invoiceItem->getRate());
$invoiceItem->setHourlyRate($invoiceItem->getRate());
}
$invoiceItem->setHourlyRate($invoiceItem->getRate());
return [$invoiceItem];
}

View File

@@ -13,7 +13,10 @@ use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\User;
class InvoiceItem
/**
* @internal
*/
final class InvoiceItem
{
/**
* @var float
@@ -28,7 +31,7 @@ class InvoiceItem
*/
private $rate = 0.00;
/**
* @var int
* @var float
*/
private $amount = 0;
/**
@@ -141,12 +144,12 @@ class InvoiceItem
return $this;
}
public function getAmount(): int
public function getAmount(): float
{
return $this->amount;
}
public function setAmount(int $amount): InvoiceItem
public function setAmount(float $amount): InvoiceItem
{
$this->amount = $amount;

View File

@@ -0,0 +1,15 @@
<?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\Invoice;
interface InvoiceItemWithAmountInterface
{
public function getAmount(): float;
}

View File

@@ -44,24 +44,24 @@ final class Version20180701120000 extends AbstractMigration
$this->addSql('CREATE TABLE ' . $userPreferences . ' (id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_8D08F631A76ED395 ON ' . $userPreferences . ' (user_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 ON ' . $userPreferences . ' (user_id, name)');
$this->addSql('CREATE TABLE ' . $customers . ' (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address CLOB DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER NOT NULL, customer_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE ' . $customers . ' (id INTEGER NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address CLOB DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER NOT NULL, customer_id INTEGER DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)');
$this->addSql('CREATE TABLE ' . $activities . ' (id INTEGER NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE ' . $activities . ' (id INTEGER NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(150) NOT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_8811FE1C166D1F9C ON ' . $activities . ' (project_id)');
$this->addSql('CREATE TABLE ' . $timesheets . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheets . ' (user)');
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheets . ' (activity_id)');
$this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address CLOB DEFAULT NULL, due_days INTEGER NOT NULL, vat INTEGER DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms CLOB DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INTEGER NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address CLOB DEFAULT NULL, due_days INTEGER NOT NULL, vat INTEGER DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms CLOB DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_1626CFE95E237E06 ON ' . $invoiceTemplates . ' (name)');
} else {
$this->addSql('CREATE TABLE ' . $users . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active TINYINT(1) NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 (name), UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 (mail), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $userPreferences . ' (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, INDEX IDX_8D08F631A76ED395 (user_id), UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 (user_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $customers . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $projects . ' (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $activities . ' (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $customers . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $projects . ' (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $activities . ' (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $timesheets . ' (id INT AUTO_INCREMENT NOT NULL, user INT DEFAULT NULL, activity_id INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INT DEFAULT NULL, description TEXT DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, INDEX IDX_4F60C6B18D93D649 (user), INDEX IDX_4F60C6B181C06096 (activity_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE ' . $userPreferences . ' ADD CONSTRAINT FK_8D08F631A76ED395 FOREIGN KEY (user_id) REFERENCES ' . $users . ' (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE ' . $activities . ' ADD CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');

View File

@@ -18,7 +18,7 @@ use Doctrine\DBAL\Schema\Schema;
/**
* Migrations fot the "delete user" feature.
*
* Adds constraints to the timesheet table, so all timesheet entries. will be deleted when a user is deleted.
* Adds constraints to the timesheet table, so all timesheet entries will be deleted when a user is deleted.
*/
final class Version20180730044139 extends AbstractMigration
{

View File

@@ -32,6 +32,7 @@ final class Version20180924111853 extends AbstractMigration
$invoiceTemplates = $this->getTableName('invoice_templates');
if ($platform === 'sqlite') {
$this->addSql('UPDATE ' . $invoiceTemplates . ' SET name=substr(name, 1, 60)');
$this->addSql('DROP INDEX UNIQ_1626CFE95E237E06');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $invoiceTemplates . ' AS SELECT id, name, title, company, address, due_days, vat, calculator, number_generator, renderer, payment_terms FROM ' . $invoiceTemplates);
$this->addSql('DROP TABLE ' . $invoiceTemplates);

View File

@@ -69,6 +69,7 @@ class Extensions extends AbstractExtension
new TwigFilter('currency', [$this, 'currency']),
new TwigFilter('country', [$this, 'country']),
new TwigFilter('language', [$this, 'language']),
new TwigFilter('amount', [$this, 'amount']),
new TwigFilter('docu_link', [$this, 'documentationLink']),
];
}
@@ -128,6 +129,15 @@ class Extensions extends AbstractExtension
return $this->getNumberFormatter()->format(number_format($duration / 3600, 2));
}
/**
* @param string|float $amount
* @return bool|false|string
*/
public function amount($amount)
{
return $this->getNumberFormatter()->format($amount);
}
private function getSecondsForDuration($duration): int
{
if (null === $duration) {

View File

@@ -42,7 +42,8 @@ final class IconExtension extends AbstractExtension
'filter' => 'fas fa-filter',
'help' => 'far fa-question-circle',
'home' => 'fas fa-home',
'invoice' => 'fas fa-file-invoice',
'invoice' => 'fas fa-file-invoice-dollar',
'invoice-template' => 'fas fa-file-signature',
'list' => 'fas fa-list',
'logout' => 'fas fa-sign-out-alt',
'mail' => 'fas fa-envelope-open',

View File

@@ -5,7 +5,7 @@
{% set actions = {'visibility': '#modal_invoice'} %}
{% if is_granted('manage_invoice_template') %}
{% set actions = actions|merge({'list': path('admin_invoice_template')}) %}
{% set actions = actions|merge({'invoice-template': path('admin_invoice_template')}) %}
{% endif %}
{% set actions = actions|merge({'help': {'url': 'invoices.html'|docu_link, 'target': '_blank'}}) %}

View File

@@ -67,21 +67,19 @@
<tr>
<th>{{ 'label.date'|trans }}</th>
<th>{{ 'label.description'|trans }}</th>
<th>{{ 'label.unit_price'|trans }}</th>
<th class="text-center">{{ 'label.hours'|trans }}</th>
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
<th class="text-right">{{ 'label.amount'|trans }}</th>
<th class="text-right">{{ 'label.total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
{% for entry in model.calculator.entries %}
{% set duration = entry.duration|duration %}
{% set duration = entry.duration|duration_decimal %}
{% if entry.fixedRate %}
{% set rate = entry.fixedRate %}
{% set duration = entry.amount %}
{% elseif entry.hourlyRate is not null %}
{% set rate = entry.hourlyRate %}
{% set duration = entry.amount|amount %}
{% else %}
{% set rate = app.user.getPreferenceValue('hourly_rate') %}
{% set rate = entry.hourlyRate %}
{% endif %}
<tr>
<td nowrap class="text-nowrap">{{ entry.begin|date_short }}</td>
@@ -92,8 +90,8 @@
{{ entry.activity.name }} / {{ entry.project.name }}
{% endif %}
</td>
<td nowrap class="text-nowrap" contenteditable="true">{{ rate|money(model.calculator.currency) }}</td>
<td nowrap class="text-nowrap text-center">{{ duration }}</td>
<td nowrap class="text-nowrap text-right">{{ rate|money(model.calculator.currency) }}</td>
<td nowrap class="text-nowrap text-right">{{ duration }}</td>
<td nowrap class="text-nowrap text-right">{{ entry.rate|money(model.calculator.currency) }}</td>
</tr>
{% endfor %}
@@ -127,9 +125,9 @@
<div class="row">
<div class="col-xs-12">
{% if model.template.paymentTerms is not empty %}
<p contenteditable="true" class="paymentTerms">
<div contenteditable="true" class="paymentTerms">
{{ model.template.paymentTerms|nl2br|md2html }}
</p>
</div>
{% endif %}
</div>
</div>

View File

@@ -61,7 +61,7 @@
{% endif %}
</table>
</article>
<article>
<article class="invoice-items">
<h2>{{ model.template.title|default('timesheet'|trans({}, 'invoice-renderer')) }}</h2>
<p contenteditable="true">
{{ 'label.invoice_salutation'|trans|nl2br }}
@@ -70,9 +70,9 @@
<thead>
<tr>
<th>{{ 'label.description'|trans }}</th>
<th>{{ 'label.unit_price'|trans }}</th>
<th>{{ 'label.amount'|trans }}</th>
<th>{{ 'label.total_rate'|trans }}</th>
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
<th class="text-right">{{ 'label.amount'|trans }}</th>
<th class="text-right">{{ 'label.total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -80,11 +80,9 @@
{% set duration = entry.duration|duration %}
{% if entry.fixedRate is not null %}
{% set rate = entry.fixedRate %}
{% set duration = entry.amount %}
{% elseif entry.hourlyRate is not null %}
{% set rate = entry.hourlyRate %}
{% set duration = entry.amount|amount %}
{% else %}
{% set rate = app.user.getPreferenceValue('hourly_rate') %}
{% set rate = entry.hourlyRate %}
{% endif %}
<tr>
@@ -95,9 +93,9 @@
{{ entry.activity.name }} / {{ entry.project.name }}
{% endif %}
</td>
<td contenteditable="true">{{ rate|money(model.calculator.currency) }}</td>
<td contenteditable="true">{{ duration }}</td>
<td>{{ entry.rate|money(model.calculator.currency) }}</td>
<td class="text-right">{{ rate|money(model.calculator.currency) }}</td>
<td class="text-right">{{ duration }}</td>
<td class="text-right">{{ entry.rate|money(model.calculator.currency) }}</td>
</tr>
{% endfor %}
</tbody>
@@ -119,9 +117,9 @@
</article>
{% if model.template.paymentTerms is not empty %}
<article class="paymentTerms">
<p contenteditable="true">
<div contenteditable="true">
{{ model.template.paymentTerms|nl2br|md2html }}
</p>
</div>
{% if signature is not empty %}
<img src="{{ signature }}">
{% endif %}

View File

@@ -72,7 +72,11 @@
{% set dayRate = {} %}
{%- endif -%}
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit{% if not entry.end %} recording{% endif %}" data-href="{{ path(editRoute, {'id': entry.id}) }}"{% endif %}>
<td class="text-nowrap">{{ tables.datatable_multiupdate_row(entry.id) }}</td>
<td class="text-nowrap">
{% if is_granted('edit', entry) or is_granted('delete', entry) %}
{{ tables.datatable_multiupdate_row(entry.id) }}
{% endif %}
</td>
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
{% if showStartEndTime %}

View File

@@ -16,6 +16,7 @@ use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Invoice\Calculator\ShortInvoiceCalculator;
use App\Invoice\InvoiceItem;
use App\Invoice\InvoiceModel;
use App\Repository\Query\InvoiceQuery;
@@ -48,6 +49,88 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
$timesheet
->setDuration(3600)
->setRate(293.27)
->setHourlyRate(293.27)
->setUser(new User())
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime())
->setEnd(new \DateTime())
;
$timesheet2 = new Timesheet();
$timesheet2
->setDuration(400)
->setRate(32.59)
->setHourlyRate(293.27)
->setUser(new User())
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime())
->setEnd(new \DateTime())
;
$timesheet3 = new Timesheet();
$timesheet3
->setDuration(1800)
->setRate(146.64)
->setHourlyRate(293.27)
->setUser(new User())
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime())
->setEnd(new \DateTime())
;
$entries = [$timesheet, $timesheet2, $timesheet3];
$query = new InvoiceQuery();
$query->setActivity($activity);
$model = new InvoiceModel();
$model->setCustomer($customer);
$model->setTemplate($template);
$model->setEntries($entries);
$model->setQuery($query);
$sut = new ShortInvoiceCalculator();
$sut->setModel($model);
$this->assertEquals('short', $sut->getId());
$this->assertEquals(562.28, $sut->getTotal());
$this->assertEquals(19, $sut->getVat());
$this->assertEquals('EUR', $sut->getCurrency());
$this->assertEquals(472.5, $sut->getSubtotal());
$this->assertEquals(5800, $sut->getTimeWorked());
$this->assertEquals(1, count($sut->getEntries()));
/** @var InvoiceItem $result */
$result = $sut->getEntries()[0];
$this->assertEquals('activity description', $result->getDescription());
$this->assertEquals(293.27, $result->getHourlyRate());
$this->assertNull($result->getFixedRate());
$this->assertEquals(472.5, $result->getRate());
$this->assertEquals(5800, $result->getDuration());
$this->assertEquals(3, $result->getAmount());
}
public function testWithMultipleEntriesDifferentRates()
{
$customer = new Customer();
$template = new InvoiceTemplate();
$template->setVat(19);
$project = new Project();
$project->setName('sdfsdf');
$activity = new Activity();
$activity->setName('activity description');
$activity->setProject($project);
$timesheet = new Timesheet();
$timesheet
->setDuration(3600)
->setRate(293.27)
->setHourlyRate(293.27)
->setUser(new User())
->setActivity($activity)
->setProject($project)
@@ -59,6 +142,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
$timesheet2
->setDuration(400)
->setRate(84)
->setHourlyRate(756.00)
->setUser(new User())
->setActivity($activity)
->setProject($project)
@@ -70,6 +154,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
$timesheet3
->setDuration(1800)
->setRate(111.11)
->setHourlyRate(222.22)
->setUser(new User())
->setActivity($activity)
->setProject($project)
@@ -99,13 +184,14 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
$this->assertEquals(5800, $sut->getTimeWorked());
$this->assertEquals(1, count($sut->getEntries()));
/** @var Timesheet $result */
/** @var InvoiceItem $result */
$result = $sut->getEntries()[0];
$this->assertEquals('activity description', $result->getDescription());
$this->assertEquals(488.38, $result->getHourlyRate());
$this->assertEquals(488.38, $result->getFixedRate());
$this->assertEquals(488.38, $result->getRate());
$this->assertEquals(5800, $result->getDuration());
$this->assertNull($result->getFixedRate());
$this->assertEquals(1, $result->getAmount());
}
public function testWithMixedRateTypes()
@@ -177,13 +263,14 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
$this->assertEquals(5400, $sut->getTimeWorked());
$this->assertEquals(1, count($sut->getEntries()));
/** @var Timesheet $result */
/** @var InvoiceItem $result */
$result = $sut->getEntries()[0];
$this->assertEquals('activity description', $result->getDescription());
$this->assertEquals(488.38, $result->getHourlyRate());
$this->assertEquals(488.38, $result->getRate());
$this->assertEquals(5800, $result->getDuration());
$this->assertEquals(488.38, $result->getFixedRate());
$this->assertEquals(1, $result->getAmount());
}
public function testDescriptionByTimesheet()

View File

@@ -50,7 +50,7 @@ class ExtensionsTest extends TestCase
public function testGetFilters()
{
$filters = ['duration', 'duration_decimal', 'money', 'currency', 'country', 'language', 'docu_link'];
$filters = ['duration', 'duration_decimal', 'money', 'currency', 'country', 'language', 'amount', 'docu_link'];
$sut = $this->getSut($this->localeDe);
$twigFilters = $sut->getFilters();
$this->assertCount(count($filters), $twigFilters);
@@ -166,6 +166,31 @@ class ExtensionsTest extends TestCase
];
}
/**
* @dataProvider getAmountData
*/
public function testAmount($result, $amount, $locale)
{
$sut = $this->getSut($this->localeEn, $locale);
$this->assertEquals($result, $sut->amount($amount));
}
public function getAmountData()
{
return [
['0', null, 'de'],
['2.345,01', 2345.01, 'de'],
['2.345', 2345, 'de'],
['2,345', 2345, 'en'],
['2,345.009', 2345.009, 'en'],
['2.345,009', 2345.009, 'de'],
['13.75', 13.75, 'en'],
['13,75', 13.75, 'de'],
['13 933,49', 13933.49, 'ru'],
['1.234.567,891', 1234567.891234567890000, 'de'],
];
}
/**
* @dataProvider getMoneyData62_1
*/