added pdf template and other invoice improvements (#1693)
This commit is contained in:
4
.github/workflows/linting.yaml
vendored
4
.github/workflows/linting.yaml
vendored
@@ -19,10 +19,10 @@ jobs:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: none
|
||||
extensions: intl
|
||||
tools: cs2pr:1.0.1
|
||||
tools: cs2pr:1.1.0
|
||||
- run: composer install --no-progress
|
||||
- run: composer validate --no-check-all --strict
|
||||
- run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php_cs.dist --using-cache=no --show-progress=none --format=checkstyle | cs2pr
|
||||
- run: vendor/bin/phpstan analyse src -c phpstan.neon --level=4 --no-progress --error-format=checkstyle | cs2pr
|
||||
- run: vendor/bin/phpstan analyse src -c phpstan.neon --level=5 --no-progress --error-format=checkstyle | cs2pr
|
||||
- run: vendor/bin/phpstan analyse tests -c tests/phpstan.neon --level=4 --no-progress --error-format=checkstyle | cs2pr
|
||||
- run: composer kimai:code-lint
|
||||
|
||||
14
UPGRADING.md
14
UPGRADING.md
@@ -12,23 +12,29 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
|
||||
- Invoice renderer `CSV` was removed
|
||||
|
||||
### Developer
|
||||
|
||||
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf` changed
|
||||
|
||||
## [1.9](https://github.com/kevinpapst/kimai2/releases/tag/1.9)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- The directory `var/data/invoices/` will be used to store archived invoice files (check file permissions)
|
||||
- The default invoice number format changed. If you want to use the old one: configure `{date}` as format - see [invoice documentation](https://www.kimai.org/documentation/invoices.html)
|
||||
- HTML invoice templates are now treated like other files and offered as download. If you are using relative URLs for including
|
||||
assets (CSS, images) you need to either inline them (see the default templates) or use absolute URLs.
|
||||
- Invoice templates that use the templates variables `${activity.X}` or `${project.X}` should be checked and possibly adapted, as multi-select is now possible for filtering
|
||||
- HTML invoice templates are now treated like other files and offered as download. If you are using relative URLs for including assets (CSS, images) you need to either inline them (see the default templates) or use absolute URLs
|
||||
- Invoice templates that use the templates variables `${activity.X}` or `${project.X}` should be checked and possibly adapted, as multi-select is now possible for filtering
|
||||
- Invoice templates have access to all meta-fields as variables, not only the ones marked as visible
|
||||
- Rates configuration/structure changed for customer, project and activity
|
||||
- **Invoice templates**: rates variables were removed
|
||||
|
||||
Permission changes:
|
||||
- `history_invoice` - NEW: grants all features for the new invoice archive (by default for all admins)
|
||||
|
||||
### Developer
|
||||
|
||||
- BC break: `InvoiceItemInterface` has new methods `getType()` and `getCategory()`
|
||||
- **BC break**: `InvoiceItemInterface` has new methods `getType()` and `getCategory()`
|
||||
- **BC break**: API fields changed - see new `/rates` endpoints
|
||||
|
||||
## [1.8](https://github.com/kevinpapst/kimai2/releases/tag/1.8)
|
||||
|
||||
|
||||
2
assets/invoice-pdf.js
Normal file
2
assets/invoice-pdf.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
require('./sass/invoice-pdf.scss');
|
||||
104
assets/sass/invoice-pdf.scss
Normal file
104
assets/sass/invoice-pdf.scss
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 10pt;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
table, tr, td, p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
th {
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.date {
|
||||
color: #666;
|
||||
font-size: 80%;
|
||||
text-align: right;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.wrapper {
|
||||
margin: 4px;
|
||||
}
|
||||
table.addresses, p {
|
||||
line-height: 14pt;
|
||||
}
|
||||
/* The address table */
|
||||
table.addresses {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
/* The PDF header */
|
||||
table.header {
|
||||
border-spacing: 0;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 100%;
|
||||
}
|
||||
table.header .title {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
}
|
||||
/* The PDF footer */
|
||||
table.footer {
|
||||
border-spacing: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
font-size: 80%;
|
||||
padding-top: 8px;
|
||||
width: 100%;
|
||||
line-height: 14px;
|
||||
}
|
||||
table.footer td {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
/* The invoice items list */
|
||||
table.items {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
table.items thead th,
|
||||
table.items tbody td {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
table.items thead th.first,
|
||||
table.items tbody td.first {
|
||||
padding-left: 0;
|
||||
}
|
||||
table.items thead th.last,
|
||||
table.items tbody td.last,
|
||||
table.items tfoot td.last{
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
table.items td,
|
||||
table.items th {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
table.items tfoot td,
|
||||
table.items tfoot th {
|
||||
padding: 20px 0 0 0;
|
||||
line-height: 1px;
|
||||
}
|
||||
table.items tr.odd {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ body.invoice_print {
|
||||
max-width: 210mm;
|
||||
box-shadow: 0 0 20px rgba(80,80,80,0.7);
|
||||
background: #fff;
|
||||
border: 1px solid #f4f4f4;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
|
||||
@@ -17,7 +17,16 @@
|
||||
"build/invoice.50473330.js"
|
||||
],
|
||||
"css": [
|
||||
"build/invoice.3764169b.css"
|
||||
"build/invoice.13d8ef4e.css"
|
||||
]
|
||||
},
|
||||
"invoice-pdf": {
|
||||
"js": [
|
||||
"build/runtime.664a9501.js",
|
||||
"build/invoice-pdf.1db2b744.js"
|
||||
],
|
||||
"css": [
|
||||
"build/invoice-pdf.340721a4.css"
|
||||
]
|
||||
},
|
||||
"chart": {
|
||||
@@ -33,10 +42,10 @@
|
||||
"build/runtime.664a9501.js",
|
||||
"build/0.f7fae2b9.js",
|
||||
"build/1.c24a9c6f.js",
|
||||
"build/calendar.d2540934.js"
|
||||
"build/calendar.33aa6a16.js"
|
||||
],
|
||||
"css": [
|
||||
"build/calendar.8ede69b5.css"
|
||||
"build/calendar.c8f3c1fd.css"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -47,10 +56,12 @@
|
||||
"build/app.d8c0b5e5.js": "sha384-VwB8dUrwHl8kZCpZjOlqXKfC39MtJNY/TSQj0oEwyFuqKGhrf0LS5++I06EOZo9b",
|
||||
"build/app.a2628675.css": "sha384-0MsxtEEys+bTKR6Qi9LvMRg21TPh2x7JxTdVFTmySyjct4BpB9xuffwyk1BDsZDe",
|
||||
"build/invoice.50473330.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
|
||||
"build/invoice.3764169b.css": "sha384-oILxBeIu3sNXxVnxhHmyHxqvLY2Rm5P2lzuJVtbxBIcnSdLS31SUSW6nVWNej1eg",
|
||||
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
|
||||
"build/invoice-pdf.1db2b744.js": "sha384-bSdIeRCtEJiYYuc2reb0e5CpJ1Kbd1lQNEkElMTiq1SX0IINzdwJJYf6WnCcHrNC",
|
||||
"build/invoice-pdf.340721a4.css": "sha384-+dk8iSafBIWj8bKpeEAkhkOR6G6o2OyL1ZeGuaIHuECtidl5cqoGbLSw+yKfSc0e",
|
||||
"build/2.a47ad919.js": "sha384-1h2P/tsl+bh8qgJd7S9irQHKuns7UATVxeFGLOCH85GPFXfAdRzgzx3nk5MrxzrV",
|
||||
"build/chart.c9943f57.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
"build/calendar.d2540934.js": "sha384-+lalaW5vVwveu7esC/1Ys27a50kO9Nd5MiWGi0yih3GNNliOKE+hY9X0Eq47RflV",
|
||||
"build/calendar.8ede69b5.css": "sha384-ta519W5+NY1a8FBh7rD4zORYTfUXCYTLci0Z25nf4BfodmuXqyPVNEe7dSe2/fRd"
|
||||
"build/calendar.33aa6a16.js": "sha384-+lalaW5vVwveu7esC/1Ys27a50kO9Nd5MiWGi0yih3GNNliOKE+hY9X0Eq47RflV",
|
||||
"build/calendar.c8f3c1fd.css": "sha384-ta519W5+NY1a8FBh7rD4zORYTfUXCYTLci0Z25nf4BfodmuXqyPVNEe7dSe2/fRd"
|
||||
}
|
||||
}
|
||||
1
public/build/invoice-pdf.1db2b744.js
Normal file
1
public/build/invoice-pdf.1db2b744.js
Normal file
@@ -0,0 +1 @@
|
||||
(window.webpackJsonp=window.webpackJsonp||[]).push([["invoice-pdf"],{G2iK:function(n,i,o){},"ls+r":function(n,i,o){o("G2iK")}},[["ls+r","runtime"]]]);
|
||||
1
public/build/invoice-pdf.340721a4.css
Normal file
1
public/build/invoice-pdf.340721a4.css
Normal file
@@ -0,0 +1 @@
|
||||
body{font-family:sans-serif;font-size:10pt}body,p,table,td,tr{margin:0;padding:0}th{border:none;font-weight:700;text-align:left}td{vertical-align:top}.text-right{text-align:right}.text-nowrap{white-space:nowrap}.date{color:#666;font-size:80%;text-align:right;padding-top:6px}.wrapper{margin:4px}p,table.addresses{line-height:14pt}table.addresses{width:100%;margin-bottom:30px}table.header{border-spacing:0;padding-bottom:4px;border-bottom:1px solid #ccc;width:100%}table.header .title{font-weight:400;font-size:140%}table.footer{border-spacing:0;border-top:1px solid #ccc;font-size:80%;padding-top:8px;width:100%;line-height:14px}table.footer td{vertical-align:bottom}table.items{width:100%;border-collapse:collapse;margin-top:50px;margin-bottom:50px}table.items tbody td,table.items thead th{border-bottom:1px solid #ccc}table.items tbody td.first,table.items thead th.first{padding-left:0}table.items tbody td.last,table.items tfoot td.last,table.items thead th.last{padding-right:0}table.items td,table.items th{padding:8px 6px}table.items tfoot td,table.items tfoot th{padding:20px 0 0;line-height:1px}table.items tr.odd{background-color:#f5f5f5}
|
||||
File diff suppressed because one or more lines are too long
@@ -4,11 +4,13 @@
|
||||
"build/2.a47ad919.js": "build/2.a47ad919.js",
|
||||
"build/app.css": "build/app.a2628675.css",
|
||||
"build/app.js": "build/app.d8c0b5e5.js",
|
||||
"build/calendar.css": "build/calendar.8ede69b5.css",
|
||||
"build/calendar.js": "build/calendar.d2540934.js",
|
||||
"build/calendar.css": "build/calendar.c8f3c1fd.css",
|
||||
"build/calendar.js": "build/calendar.33aa6a16.js",
|
||||
"build/chart.js": "build/chart.c9943f57.js",
|
||||
"build/invoice.css": "build/invoice.3764169b.css",
|
||||
"build/invoice.css": "build/invoice.13d8ef4e.css",
|
||||
"build/invoice.js": "build/invoice.50473330.js",
|
||||
"build/invoice-pdf.css": "build/invoice-pdf.340721a4.css",
|
||||
"build/invoice-pdf.js": "build/invoice-pdf.1db2b744.js",
|
||||
"build/runtime.js": "build/runtime.664a9501.js",
|
||||
"build/fonts/fa-brands-400.eot": "build/fonts/fa-brands-400.088a34f7.eot",
|
||||
"build/fonts/fa-brands-400.ttf": "build/fonts/fa-brands-400.273dc9bf.ttf",
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Entity\Project;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\InvoiceTemplateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
@@ -46,6 +47,10 @@ class InvoiceCreateCommand extends Command
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
private $customerRepository;
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $projectRepository;
|
||||
/**
|
||||
* @var InvoiceTemplateRepository
|
||||
*/
|
||||
@@ -67,6 +72,7 @@ class InvoiceCreateCommand extends Command
|
||||
ServiceInvoice $serviceInvoice,
|
||||
TimesheetRepository $timesheetRepository,
|
||||
CustomerRepository $customerRepository,
|
||||
ProjectRepository $projectRepository,
|
||||
InvoiceTemplateRepository $invoiceTemplateRepository,
|
||||
UserRepository $userRepository,
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
@@ -74,6 +80,7 @@ class InvoiceCreateCommand extends Command
|
||||
$this->serviceInvoice = $serviceInvoice;
|
||||
$this->timesheetRepository = $timesheetRepository;
|
||||
$this->customerRepository = $customerRepository;
|
||||
$this->projectRepository = $projectRepository;
|
||||
$this->invoiceTemplateRepository = $invoiceTemplateRepository;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
@@ -94,6 +101,7 @@ class InvoiceCreateCommand extends Command
|
||||
->addOption('end', null, InputOption::VALUE_OPTIONAL, 'End date (format: 2020-01-31, default: end of the month)', null)
|
||||
->addOption('timezone', null, InputOption::VALUE_OPTIONAL, 'Timezone for start and end date query', date_default_timezone_get())
|
||||
->addOption('customer', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of customer IDs', null)
|
||||
->addOption('project', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of project IDs', null)
|
||||
->addOption('by-customer', null, InputOption::VALUE_NONE, 'If set, one invoice for each active customer in the given timerange is created')
|
||||
->addOption('by-project', null, InputOption::VALUE_NONE, 'If set, one invoice for each active project in the given timerange is created')
|
||||
->addOption('set-exported', null, InputOption::VALUE_NONE, 'Whether the invoice items should be marked as exported')
|
||||
@@ -168,8 +176,9 @@ class InvoiceCreateCommand extends Command
|
||||
}
|
||||
|
||||
$customersIDs = $input->getOption('customer');
|
||||
if (!$byActiveCustomer && !$byActiveProject && empty($customersIDs)) {
|
||||
$io->error('Could not determine generation mode, you need to set one of: customer, by-customer, by-project');
|
||||
$projectIDs = $input->getOption('project');
|
||||
if (!$byActiveCustomer && !$byActiveProject && empty($customersIDs) && empty($projectIDs)) {
|
||||
$io->error('Could not determine generation mode, you need to set one of: customer, project, by-customer, by-project');
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -235,15 +244,15 @@ class InvoiceCreateCommand extends Command
|
||||
$defaultQuery->setCurrentUser($user);
|
||||
$defaultQuery->setSearchTerm($searchTerm);
|
||||
$defaultQuery->setMarkAsExported($markAsExported);
|
||||
$defaultQuery->setState($exportedFilter);
|
||||
$defaultQuery->setExported($exportedFilter);
|
||||
|
||||
/** @var Invoice[] $invoices */
|
||||
$invoices = [];
|
||||
|
||||
/** @var Customer[] $customers */
|
||||
$customers = [];
|
||||
|
||||
if (!empty($customersIDs)) {
|
||||
/** @var Customer[] $customers */
|
||||
$customers = [];
|
||||
|
||||
$customersIDs = explode(',', $customersIDs);
|
||||
foreach ($customersIDs as $id) {
|
||||
$tmp = $this->customerRepository->find($id);
|
||||
@@ -255,6 +264,21 @@ class InvoiceCreateCommand extends Command
|
||||
$customers[] = $tmp;
|
||||
}
|
||||
$invoices = $this->createInvoicesForCustomer($customers, $defaultQuery, $input, $output);
|
||||
} elseif (!empty($projectIDs)) {
|
||||
/** @var Project[] $projects */
|
||||
$projects = [];
|
||||
|
||||
$projectIDs = explode(',', $projectIDs);
|
||||
foreach ($projectIDs as $id) {
|
||||
$tmp = $this->projectRepository->find($id);
|
||||
if (null === $tmp) {
|
||||
$io->error('Unknown project ID: ' . $id);
|
||||
|
||||
return 1;
|
||||
}
|
||||
$projects[] = $tmp;
|
||||
}
|
||||
$invoices = $this->createInvoicesForProjects($projects, $defaultQuery, $input, $output);
|
||||
} elseif ($byActiveCustomer) {
|
||||
$customers = $this->getActiveCustomers($start, $end);
|
||||
$invoices = $this->createInvoicesForCustomer($customers, $defaultQuery, $input, $output);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
@@ -19,6 +20,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @UniqueEntity("name")
|
||||
*/
|
||||
class InvoiceTemplate
|
||||
{
|
||||
|
||||
@@ -77,7 +77,13 @@ class InvoiceRendererType extends AbstractType
|
||||
array_pop($parts);
|
||||
}
|
||||
|
||||
return ucfirst(array_pop($parts));
|
||||
$type = array_pop($parts);
|
||||
|
||||
if (\in_array(strtolower($type), ['json', 'txt', 'xml'])) {
|
||||
return 'programmatic';
|
||||
}
|
||||
|
||||
return ucfirst($type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,7 +75,7 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
||||
'entry.currency' => $currency,
|
||||
'entry.duration' => $item->getDuration(),
|
||||
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($item->getDuration()),
|
||||
'entry.duration_minutes' => number_format($item->getDuration() / 60),
|
||||
'entry.duration_minutes' => (int) ($item->getDuration() / 60),
|
||||
'entry.begin' => $formatter->getFormattedDateTime($begin),
|
||||
'entry.begin_time' => $formatter->getFormattedTime($begin),
|
||||
'entry.begin_timestamp' => $begin->getTimestamp(),
|
||||
|
||||
@@ -57,7 +57,7 @@ class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
$prefix . 'budget_money_plain' => $project->getBudget(),
|
||||
$prefix . 'budget_time' => $project->getTimeBudget(),
|
||||
$prefix . 'budget_time_decimal' => $formatter->getFormattedDecimalDuration($project->getTimeBudget()),
|
||||
$prefix . 'budget_time_minutes' => number_format($project->getTimeBudget() / 60),
|
||||
$prefix . 'budget_time_minutes' => (int) ($project->getTimeBudget() / 60),
|
||||
];
|
||||
|
||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||
|
||||
@@ -45,7 +45,14 @@ final class PdfRenderer implements RendererInterface
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
'model' => $model
|
||||
]);
|
||||
$content = $this->converter->convertToPdf($content);
|
||||
|
||||
$content = $this->converter->convertToPdf($content, [
|
||||
'setAutoTopMargin' => 'pad',
|
||||
'setAutoBottomMargin' => 'pad',
|
||||
'margin_top' => 12,
|
||||
'margin_bottom' => 8,
|
||||
]);
|
||||
|
||||
$filename = (string) new InvoiceFilename($model);
|
||||
|
||||
$response = new Response($content);
|
||||
|
||||
@@ -91,7 +91,11 @@ class DateExtensions extends AbstractExtension
|
||||
}
|
||||
|
||||
if (!$date instanceof DateTime) {
|
||||
$date = new DateTime($date);
|
||||
try {
|
||||
$date = new DateTime($date);
|
||||
} catch (\Exception $ex) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
return date_format($date, $this->dateFormat);
|
||||
@@ -109,7 +113,11 @@ class DateExtensions extends AbstractExtension
|
||||
}
|
||||
|
||||
if (!$date instanceof DateTime) {
|
||||
$date = new DateTime($date);
|
||||
try {
|
||||
$date = new DateTime($date);
|
||||
} catch (\Exception $ex) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
return $date->format($this->dateTimeFormat);
|
||||
@@ -128,7 +136,11 @@ class DateExtensions extends AbstractExtension
|
||||
}
|
||||
|
||||
if (!$date instanceof DateTime) {
|
||||
$date = new DateTime($date);
|
||||
try {
|
||||
$date = new DateTime($date);
|
||||
} catch (\Exception $ex) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
$timezone = date_default_timezone_get();
|
||||
@@ -158,7 +170,11 @@ class DateExtensions extends AbstractExtension
|
||||
public function dateFormat($date, string $format)
|
||||
{
|
||||
if (!$date instanceof DateTime) {
|
||||
$date = new DateTime($date);
|
||||
try {
|
||||
$date = new DateTime($date);
|
||||
} catch (\Exception $ex) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
return date_format($date, $format);
|
||||
|
||||
@@ -193,7 +193,12 @@ class Extensions extends AbstractExtension
|
||||
*/
|
||||
public function currency($currency)
|
||||
{
|
||||
return Currencies::getSymbol($currency);
|
||||
try {
|
||||
return Currencies::getSymbol(strtoupper($currency));
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return $currency;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,7 +207,12 @@ class Extensions extends AbstractExtension
|
||||
*/
|
||||
public function language($language)
|
||||
{
|
||||
return Languages::getName($language, $this->locale);
|
||||
try {
|
||||
return Languages::getName(strtolower($language), $this->locale);
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return $language;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,9 +221,9 @@ class Extensions extends AbstractExtension
|
||||
*/
|
||||
public function country($country)
|
||||
{
|
||||
$country = strtoupper($country);
|
||||
if (Countries::exists($country)) {
|
||||
return Countries::getName($country);
|
||||
try {
|
||||
return Countries::getName(strtoupper($country));
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return $country;
|
||||
|
||||
@@ -14,7 +14,8 @@ interface HtmlToPdfConverter
|
||||
/**
|
||||
* Returns the binary content of the PDF, which can be saved as file or send via Reponse.
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return mixed
|
||||
*/
|
||||
public function convertToPdf(string $html);
|
||||
public function convertToPdf(string $html, array $options = []);
|
||||
}
|
||||
|
||||
@@ -27,12 +27,14 @@ class MPdfConverter implements HtmlToPdfConverter
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return mixed|string
|
||||
* @throws \Mpdf\MpdfException
|
||||
*/
|
||||
public function convertToPdf(string $html)
|
||||
public function convertToPdf(string $html, array $options = [])
|
||||
{
|
||||
$mpdf = new Mpdf(['tempDir' => $this->cacheDirectory]);
|
||||
$options = array_merge($options, ['tempDir' => $this->cacheDirectory]);
|
||||
$mpdf = new Mpdf($options);
|
||||
$mpdf->creator = Constants::SOFTWARE;
|
||||
|
||||
// some OS do not follow the PHP default settings
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
{% block main %}
|
||||
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found('kimai.activityUpdate') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.activityUpdate'}) }}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% block main %}
|
||||
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found('kimai.customerUpdate kimai.customerTeamUpdate') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.customerUpdate kimai.customerTeamUpdate'}) }}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{% block main %}
|
||||
|
||||
{% if widgets is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
{% for row in widgets %}
|
||||
{{ render_widget(row) }}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
{% if preview_show %}
|
||||
{% if entries is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {}) }}
|
||||
{% set totalAmount = {} %}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{% set actions = actions|merge({'visibility': '#modal_invoice_template'}) %}
|
||||
|
||||
{% if is_granted('manage_invoice_template') %}
|
||||
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
|
||||
{% set actions = actions|merge({'create': {'url': path('admin_invoice_template_create'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('upload_invoice_template') %}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
{% if preview %}
|
||||
{% if model.calculator is empty or model.calculator.entries is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||
{% set entries = model.calculator.entries %}
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
{% block main %}
|
||||
|
||||
{% if entries is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {}) }}
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<tr class="alternative-link open-edit" data-href="{{ path('admin_invoice_download', {'id': entry.id}) }}">
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.createdAt|date_short }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.user_avatar(entry.user) }} {{ widgets.username(entry.user) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
|
||||
|
||||
160
templates/invoice/renderer/default-pdf.pdf.twig
Normal file
160
templates/invoice/renderer/default-pdf.pdf.twig
Normal file
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
|
||||
{% set language = model.template.language|default(fallback) %}
|
||||
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||
{% set currency = model.currency %}
|
||||
<html lang="{{ language }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
<title>{% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %}</title>
|
||||
<style type="text/css">
|
||||
{{ encore_entry_css_source('invoice-pdf')|raw }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--mpdf
|
||||
<htmlpageheader name="header">
|
||||
<table class="header">
|
||||
<tr>
|
||||
<td class="title">
|
||||
{{ model.template.title }}
|
||||
</td>
|
||||
<td class="date">
|
||||
{{ 'label.date'|trans({}, 'messages', language) }}: {{ model.invoiceDate|date_short }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</htmlpageheader>
|
||||
<sethtmlpageheader name="header" page="ALL" value="on" show-this-page="1" />
|
||||
<htmlpagefooter name="footer">
|
||||
<table class="footer">
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ 'label.contact'|trans({}, 'messages', language) }}</strong>:
|
||||
{{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<br>
|
||||
<strong>{{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}</strong>:
|
||||
{{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
</td>
|
||||
<td align="right">
|
||||
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</htmlpagefooter>
|
||||
<sethtmlpagefooter page="ALL" value="on" name="footer" show-this-page="1" />
|
||||
mpdf-->
|
||||
<div class="wrapper">
|
||||
<table class="addresses">
|
||||
<tr>
|
||||
<td width="60%">
|
||||
{{ 'invoice.to'|trans({}, 'messages', language) }}
|
||||
<br>
|
||||
<strong>{{ model.customer.company|default(model.customer.name) }}</strong>
|
||||
<br>
|
||||
{{ model.customer.address|nl2br }}
|
||||
{% if model.customer.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
|
||||
{% endif %}
|
||||
{% if model.customer.number is not empty %}
|
||||
<br>
|
||||
{{ 'label.number'|trans({}, 'messages', language) }}: {{ model.customer.number }}
|
||||
{% endif %}
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<br>
|
||||
{{ 'label.orderNumber'|trans({}, 'messages', language) }}: {{ model.query.project.orderNumber }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ 'invoice.from'|trans({}, 'messages', language) }}
|
||||
<br>
|
||||
<strong>{{ model.template.company }}</strong>
|
||||
<br>
|
||||
{{ model.template.address|trim|nl2br }}
|
||||
{% if model.template.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans({}, 'messages', language) }}:
|
||||
{{ model.template.vatId }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<strong>{{ 'invoice.number'|trans({}, 'messages', language) }}:</strong>
|
||||
{{ model.invoiceNumber }}
|
||||
|
||||
<br>
|
||||
<strong>{{ 'invoice.due_days'|trans({}, 'messages', language) }}:</strong>
|
||||
{{ model.dueDate|date_short }}
|
||||
</p>
|
||||
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="first">{{ 'label.date'|trans({}, 'messages', language) }}</th>
|
||||
<th>{{ 'label.description'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.unit_price'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.amount'|trans({}, 'messages', language) }}</th>
|
||||
<th class="last text-right">{{ 'label.total_rate'|trans({}, 'messages', language) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for id, entry in model.calculator.entries %}
|
||||
{% set duration = entry.duration|duration(isDecimal) %}
|
||||
{% if entry.fixedRate %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
{% set duration = entry.amount|amount %}
|
||||
{% else %}
|
||||
{% set rate = entry.hourlyRate %}
|
||||
{% endif %}
|
||||
<tr{# class="{{ cycle(['odd', 'even'], id) }}"#}>
|
||||
<td nowrap class="first text-nowrap">{{ entry.begin|date_short }}</td>
|
||||
<td contenteditable="true">
|
||||
{% if entry.description is not empty %}
|
||||
{{ entry.description|nl2br }}
|
||||
{% else %}
|
||||
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td nowrap class="text-nowrap text-right">{{ rate|money(currency) }}</td>
|
||||
<td nowrap class="text-nowrap text-right">{{ duration }}</td>
|
||||
<td nowrap class="last text-nowrap text-right">{{ entry.rate|money(currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
{{ 'invoice.subtotal'|trans({}, 'messages', language) }}
|
||||
</td>
|
||||
<td class="last text-right">{{ model.calculator.subtotal|money(currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)
|
||||
</td>
|
||||
<td class="last text-right">{{ model.calculator.tax|money(currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right text-nowrap">
|
||||
<strong>{{ 'invoice.total'|trans({}, 'messages', language) }}</strong>
|
||||
</td>
|
||||
<td class="last text-right">
|
||||
<strong>{{ model.calculator.total|money(currency) }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
{% if model.template.paymentTerms is not empty %}
|
||||
<p>
|
||||
{{ model.template.paymentTerms|md2html }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
{% block main %}
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found('kimai.invoiceTemplateUpdate') }}
|
||||
{% endif %}
|
||||
|
||||
{{ tables.datatable_header(tableName, columns, null, {'reload': 'kimai.invoiceTemplateUpdate'}) }}
|
||||
|
||||
@@ -458,3 +458,14 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro nothing_found(events) %}
|
||||
{{ _self.callout('warning', 'error.no_entries_found') }}
|
||||
{% if events is not empty %}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('{{ events }}');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{% block main %}
|
||||
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found('kimai.projectUpdate kimai.projectTeamUpdate') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.projectUpdate kimai.projectTeamUpdate'}) }}
|
||||
|
||||
|
||||
@@ -11,12 +11,7 @@
|
||||
{% block main %}
|
||||
|
||||
{% if tags|length == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.tagUpdate');
|
||||
});
|
||||
</script>
|
||||
{{ widgets.nothing_found('kimai.tagUpdate') }}
|
||||
{% else %}
|
||||
|
||||
{% set columns = {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% block main %}
|
||||
|
||||
{% if teams|length == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found('kimai.teamUpdate') }}
|
||||
{% else %}
|
||||
|
||||
{% set columns = {
|
||||
|
||||
@@ -57,12 +57,7 @@
|
||||
{% block main %}
|
||||
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.timesheetUpdate');
|
||||
});
|
||||
</script>
|
||||
{{ widgets.nothing_found('kimai.timesheetUpdate') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'striped': not showSummary, 'reload': 'kimai.timesheetUpdate'}) }}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
{% block main %}
|
||||
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found('kimai.userUpdate') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.userUpdate'}) }}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% block profile_content %}
|
||||
|
||||
{% if years is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% endif %}
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Entity\Project;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\InvoiceTemplateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Tests\DataFixtures\CustomerFixtures;
|
||||
@@ -70,6 +71,7 @@ class InvoiceCreateCommandTest extends KernelTestCase
|
||||
$container->get(ServiceInvoice::class),
|
||||
$container->get(TimesheetRepository::class),
|
||||
$container->get(CustomerRepository::class),
|
||||
$container->get(ProjectRepository::class),
|
||||
$container->get(InvoiceTemplateRepository::class),
|
||||
$container->get(UserRepository::class),
|
||||
$container->get('event_dispatcher')
|
||||
@@ -167,6 +169,11 @@ class InvoiceCreateCommandTest extends KernelTestCase
|
||||
$this->assertCommandErrors(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => 3, '--template' => 'x'], 'Unknown customer ID: 3');
|
||||
}
|
||||
|
||||
public function testCreateWithInvalidProject()
|
||||
{
|
||||
$this->assertCommandErrors(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--project' => 3, '--template' => 'x'], 'Unknown project ID: 3');
|
||||
}
|
||||
|
||||
public function testCreateInvoice()
|
||||
{
|
||||
$fixture = new InvoiceFixtures();
|
||||
@@ -251,6 +258,19 @@ class InvoiceCreateCommandTest extends KernelTestCase
|
||||
$this->assertStringContainsString('Created 1 invoice(s) ', $output);
|
||||
}
|
||||
|
||||
public function testCreateInvoiceByProjectId()
|
||||
{
|
||||
$start = new \DateTime('-2 months');
|
||||
$end = new \DateTime();
|
||||
|
||||
$this->prepareFixtures($start);
|
||||
|
||||
$commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--exported' => 'all', '--project' => '1', '--template' => 'Invoice', '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Created 1 invoice(s) ', $output);
|
||||
}
|
||||
|
||||
public function testCreateInvoiceByProjectWithPreview()
|
||||
{
|
||||
$start = new \DateTime('-2 months');
|
||||
|
||||
@@ -25,6 +25,7 @@ class InvoiceDocumentRepositoryTest extends TestCase
|
||||
protected static $defaultDocuments = [
|
||||
'company.docx',
|
||||
'default.html.twig',
|
||||
'default-pdf.pdf.twig',
|
||||
'freelancer.html.twig',
|
||||
'timesheet.html.twig',
|
||||
'text.txt.twig',
|
||||
|
||||
@@ -91,6 +91,7 @@ class DateExtensionsTest extends TestCase
|
||||
['de', new \DateTime('1980-12-14'), '14.12.1980'],
|
||||
['ru', new \DateTime('1980-12-14'), '14.12.1980'],
|
||||
['ru', '1980-12-14', '14.12.1980'],
|
||||
['ru', 1.2345, 1.2345],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -115,6 +116,7 @@ class DateExtensionsTest extends TestCase
|
||||
['en', new \DateTime('7 January 2010'), '2010-01-07 12:01 AM'],
|
||||
['de', (new \DateTime('1980-12-14'))->setTime(13, 27, 55), '14.12.1980 13:27:55'],
|
||||
['de', '1980-12-14 13:27:55', '14.12.1980 13:27:55'],
|
||||
['de', 1.2345, 1.2345],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -144,6 +146,7 @@ class DateExtensionsTest extends TestCase
|
||||
$sut = $this->getSut('en', []);
|
||||
$this->assertEquals('2010-01-07T17:43:21+01:00', $sut->dateFormat($date, 'c'));
|
||||
$this->assertStringStartsWith('2010-01-07T17:43:21', $sut->dateFormat('7 January 2010 17:43:21', 'c'));
|
||||
$this->assertEquals(2010.0107, $sut->dateFormat(2010.0107, 'c'));
|
||||
}
|
||||
|
||||
public function testTime()
|
||||
@@ -181,5 +184,6 @@ class DateExtensionsTest extends TestCase
|
||||
|
||||
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull($dateTime));
|
||||
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull('2019-08-17 12:29:47'));
|
||||
$this->assertEquals(189.45, $sut->dateTimeFull(189.45));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ class ExtensionsTest extends TestCase
|
||||
'EUR' => '€',
|
||||
'USD' => '$',
|
||||
'RUB' => 'RUB',
|
||||
'rub' => 'RUB',
|
||||
123 => 123,
|
||||
];
|
||||
|
||||
$sut = $this->getSut($this->localeEn);
|
||||
@@ -109,6 +111,8 @@ class ExtensionsTest extends TestCase
|
||||
'DE' => 'Germany',
|
||||
'RU' => 'Russia',
|
||||
'ES' => 'Spain',
|
||||
'es' => 'Spain',
|
||||
'12' => '12',
|
||||
];
|
||||
|
||||
$sut = $this->getSut($this->localeEn);
|
||||
@@ -123,6 +127,8 @@ class ExtensionsTest extends TestCase
|
||||
'de' => 'German',
|
||||
'ru' => 'Russian',
|
||||
'es' => 'Spanish',
|
||||
'ES' => 'Spanish',
|
||||
'12' => '12',
|
||||
];
|
||||
|
||||
$sut = $this->getSut($this->localeEn);
|
||||
|
||||
@@ -6,9 +6,59 @@
|
||||
<source>label.invoice_renderer</source>
|
||||
<target>Rechnungsdokument</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Achtung: existierende Dateien werden übschrieben. Erlaubte Dateitypen sind: DOCX, ODS, XLSX</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="programmatic">
|
||||
<source>programmatic</source>
|
||||
<target>Zur Weiterverarbeitung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Html">
|
||||
<source>Html</source>
|
||||
<target>HTML (zum Drucken via Browser)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Docx">
|
||||
<source>Docx</source>
|
||||
<target>Microsoft Word (Docx)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Xls">
|
||||
<source>Xls</source>
|
||||
<target>Microsoft Excel (Xls)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Ods">
|
||||
<source>Ods</source>
|
||||
<target>Open Office (Tabelle)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Xlsx">
|
||||
<source>Xlsx</source>
|
||||
<target>Microsoft Excel (Xlsx)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Pdf">
|
||||
<source>Pdf</source>
|
||||
<target>PDF</target>
|
||||
</trans-unit>
|
||||
<!-- Renderer -->
|
||||
<trans-unit id="default">
|
||||
<source>default</source>
|
||||
<target>Rechnung (Standard)</target>
|
||||
<target>Rechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="default-pdf">
|
||||
<source>default-pdf</source>
|
||||
<target>Rechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="xml">
|
||||
<source>xml</source>
|
||||
<target>XML</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="javascript">
|
||||
<source>javascript</source>
|
||||
<target>JSON</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="text">
|
||||
<source>text</source>
|
||||
<target>Text</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet">
|
||||
<source>timesheet</source>
|
||||
@@ -20,23 +70,15 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="open-spreadsheet">
|
||||
<source>open-spreadsheet</source>
|
||||
<target>Calc Tabellendokument</target>
|
||||
<target>Rechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="spreadsheet">
|
||||
<source>spreadsheet</source>
|
||||
<target>Excel Tabelle</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export">
|
||||
<source>export</source>
|
||||
<target>Datenexport</target>
|
||||
<target>Rechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="company">
|
||||
<source>company</source>
|
||||
<target>Firmen-Rechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Achtung: existierende Dateien werden übschrieben. Erlaubte Dateitypen sind: DOCX, ODS, XLSX</target>
|
||||
<target>Rechnung</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -6,9 +6,59 @@
|
||||
<source>label.invoice_renderer</source>
|
||||
<target>Invoice document</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Attention: existing files will be overwritten. Allowed file types are: DOCX, ODS, XLSX.</target>
|
||||
</trans-unit>
|
||||
<!-- Optgroups -->
|
||||
<trans-unit id="programmatic">
|
||||
<source>programmatic</source>
|
||||
<target>For further processing</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Html">
|
||||
<source>Html</source>
|
||||
<target>HTML (for printing via Browser)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Docx">
|
||||
<source>Docx</source>
|
||||
<target>Microsoft Word (Docx)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Xls">
|
||||
<source>Xls</source>
|
||||
<target>Microsoft Excel (Xls)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Ods">
|
||||
<source>Ods</source>
|
||||
<target>Open Office (Spreadsheet)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Xlsx">
|
||||
<source>Xlsx</source>
|
||||
<target>Microsoft Excel (Xlsx)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Pdf">
|
||||
<source>Pdf</source>
|
||||
<target>PDF</target>
|
||||
</trans-unit>
|
||||
<!-- Renderer -->
|
||||
<trans-unit id="default">
|
||||
<source>default</source>
|
||||
<target>Invoice (default)</target>
|
||||
<target>Invoice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="default-pdf">
|
||||
<source>default-pdf</source>
|
||||
<target>Invoice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="xml">
|
||||
<source>xml</source>
|
||||
<target>XML</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="javascript">
|
||||
<source>javascript</source>
|
||||
<target>JSON</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="text">
|
||||
<source>text</source>
|
||||
<target>Text</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="timesheet">
|
||||
<source>timesheet</source>
|
||||
@@ -20,23 +70,15 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="open-spreadsheet">
|
||||
<source>open-spreadsheet</source>
|
||||
<target>Calc Spreadsheet</target>
|
||||
<target>Invoice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="spreadsheet">
|
||||
<source>spreadsheet</source>
|
||||
<target>Excel</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="export">
|
||||
<source>export</source>
|
||||
<target>Data export</target>
|
||||
<target>Invoice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="company">
|
||||
<source>company</source>
|
||||
<target>Company invoice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="help.upload">
|
||||
<source>help.upload</source>
|
||||
<target>Attention: existing files will be overwritten. Allowed file types are: DOCX, ODS, XLSX.</target>
|
||||
<target>Invoice</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -11,6 +11,7 @@ Encore
|
||||
|
||||
.addEntry('app', './assets/app.js')
|
||||
.addEntry('invoice', './assets/invoice.js')
|
||||
.addEntry('invoice-pdf', './assets/invoice-pdf.js')
|
||||
.addEntry('chart', './assets/chart.js')
|
||||
.addEntry('calendar', './assets/calendar.js')
|
||||
.copyFiles({ from: './assets/images', to: 'images/[path][name].[ext]' })
|
||||
@@ -26,7 +27,7 @@ Encore
|
||||
.enableSassLoader()
|
||||
.autoProvidejQuery()
|
||||
|
||||
// prevent that moment locales will be included which we don't need
|
||||
// prevent that unused moment locales will be included
|
||||
.addPlugin(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
|
||||
|
||||
.configureBabel(null, {
|
||||
|
||||
Reference in New Issue
Block a user