create multiple invoices at once (#2465)
This commit is contained in:
@@ -8,6 +8,13 @@ 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.
|
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.
|
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||||
|
|
||||||
|
## [1.14](https://github.com/kevinpapst/kimai2/releases/tag/1.13)
|
||||||
|
|
||||||
|
**New database tables and fields: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||||
|
|
||||||
|
Permission changes:
|
||||||
|
- `history_invoice` - removed permission entirely
|
||||||
|
|
||||||
## [1.13](https://github.com/kevinpapst/kimai2/releases/tag/1.13)
|
## [1.13](https://github.com/kevinpapst/kimai2/releases/tag/1.13)
|
||||||
|
|
||||||
- Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}`
|
- Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}`
|
||||||
|
|||||||
@@ -71,35 +71,38 @@ export default class KimaiDatatableColumnView extends KimaiPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changeVisibility(columnName, checked) {
|
changeVisibility(columnName, checked) {
|
||||||
const table = document.getElementById('datatable_' + this.id).getElementsByClassName('dataTable')[0];
|
const tables = document.getElementsByClassName('datatable_' + this.id);
|
||||||
let column = 0;
|
for (let tableBox of tables) {
|
||||||
let foundColumn = false;
|
let column = 0;
|
||||||
for (let columnElement of table.getElementsByTagName('th')) {
|
let foundColumn = false;
|
||||||
if (columnElement.getAttribute('data-field') === columnName) {
|
let table = tableBox.getElementsByClassName('dataTable')[0];
|
||||||
foundColumn = true;
|
for (let columnElement of table.getElementsByTagName('th')) {
|
||||||
break;
|
if (columnElement.getAttribute('data-field') === columnName) {
|
||||||
|
foundColumn = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (columnElement.getAttribute('colspan') !== null) {
|
||||||
|
console.log('Tables with colspans are not supported!');
|
||||||
|
}
|
||||||
|
|
||||||
|
column++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (columnElement.getAttribute('colspan') !== null) {
|
if (!foundColumn) {
|
||||||
console.log('Tables with colspans are not supported!');
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
column++;
|
for (let rowElement of table.getElementsByTagName('tr')) {
|
||||||
}
|
if (rowElement.children[column] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!foundColumn) {
|
if (checked) {
|
||||||
return;
|
rowElement.children[column].classList.remove('hidden');
|
||||||
}
|
} else {
|
||||||
|
rowElement.children[column].classList.add('hidden');
|
||||||
for (let rowElement of table.getElementsByTagName('tr')) {
|
}
|
||||||
if (rowElement.children[column] === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
rowElement.children[column].classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
rowElement.children[column].classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,4 +32,47 @@ export default class KimaiForm extends KimaiPlugin {
|
|||||||
this.getContainer().getPlugin('date-time-picker').destroyDateTimePicker(formSelector);
|
this.getContainer().getPlugin('date-time-picker').destroyDateTimePicker(formSelector);
|
||||||
this.getContainer().getPlugin('date-range-picker').destroyDateRangePicker(formSelector);
|
this.getContainer().getPlugin('date-range-picker').destroyDateRangePicker(formSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFormData(form) {
|
||||||
|
let serialized = [];
|
||||||
|
|
||||||
|
// Loop through each field in the form
|
||||||
|
for (let i = 0; i < form.elements.length; i++) {
|
||||||
|
|
||||||
|
let field = form.elements[i];
|
||||||
|
|
||||||
|
// Don't serialize a couple of field types (button and submit are important to exclude, eg. invoice preview would fail otherwise)
|
||||||
|
if (!field.name || field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a multi-select, get all selections
|
||||||
|
if (field.type === 'select-multiple') {
|
||||||
|
for (var n = 0; n < field.options.length; n++) {
|
||||||
|
if (!field.options[n].selected) continue;
|
||||||
|
serialized.push({
|
||||||
|
name: field.name,
|
||||||
|
value: field.options[n].value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if ((field.type !== 'checkbox' && field.type !== 'radio') || field.checked) {
|
||||||
|
serialized.push({
|
||||||
|
name: field.name,
|
||||||
|
value: field.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertFormDataToQueryString(formData) {
|
||||||
|
let serialized = [];
|
||||||
|
|
||||||
|
for (let row of formData) {
|
||||||
|
serialized.push(encodeURIComponent(row.name) + "=" + encodeURIComponent(row.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialized.join('&');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ kimai:
|
|||||||
CUSTOMERS_ALL_TEAM: ['view_team_customer','edit_team_customer','budget_team_customer','comments_team_customer','comments_create_team_customer','details_team_customer']
|
CUSTOMERS_ALL_TEAM: ['view_team_customer','edit_team_customer','budget_team_customer','comments_team_customer','comments_create_team_customer','details_team_customer']
|
||||||
CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer']
|
CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer']
|
||||||
INVOICE: ['view_invoice','create_invoice']
|
INVOICE: ['view_invoice','create_invoice']
|
||||||
INVOICE_ADMIN: ['manage_invoice_template','history_invoice']
|
INVOICE_ADMIN: ['manage_invoice_template']
|
||||||
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
|
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
|
||||||
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
||||||
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
"build/runtime.098eaae1.js",
|
"build/runtime.098eaae1.js",
|
||||||
"build/0.79dbdbb9.js",
|
"build/0.79dbdbb9.js",
|
||||||
"build/1.32489d92.js",
|
"build/1.32489d92.js",
|
||||||
"build/app.b19b6399.js"
|
"build/app.0d1f3758.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"build/app.b22111d1.css"
|
"build/app.b22111d1.css"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
"build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||||
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
|
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
|
||||||
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
|
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
|
||||||
"build/app.b19b6399.js": "sha384-giiZt81GRzPg3SJzQLRg7+bLAhpzPPP0XCqzZBcznH/JfE3Z3wQElGPKuJR84RXl",
|
"build/app.0d1f3758.js": "sha384-lptsmHCgW8drPM/w0avBAIS0Dgsg4qQm+RQnxVeMMkmTl9kSy2UfJGaNUfncPH3x",
|
||||||
"build/app.b22111d1.css": "sha384-ZMJkA2sXnH7J/uMTKDBf8+bLNHulT+yZiz1h5fxzxJshqw9Ccs3brjAJPqb+0/6S",
|
"build/app.b22111d1.css": "sha384-ZMJkA2sXnH7J/uMTKDBf8+bLNHulT+yZiz1h5fxzxJshqw9Ccs3brjAJPqb+0/6S",
|
||||||
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
|
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
|
||||||
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
|
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"build/1.32489d92.js": "build/1.32489d92.js",
|
"build/1.32489d92.js": "build/1.32489d92.js",
|
||||||
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
|
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
|
||||||
"build/app.css": "build/app.b22111d1.css",
|
"build/app.css": "build/app.b22111d1.css",
|
||||||
"build/app.js": "build/app.b19b6399.js",
|
"build/app.js": "build/app.0d1f3758.js",
|
||||||
"build/calendar.css": "build/calendar.1408f57e.css",
|
"build/calendar.css": "build/calendar.1408f57e.css",
|
||||||
"build/calendar.js": "build/calendar.070aab88.js",
|
"build/calendar.js": "build/calendar.070aab88.js",
|
||||||
"build/chart.js": "build/chart.34d60a88.js",
|
"build/chart.js": "build/chart.34d60a88.js",
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ use App\Repository\InvoiceTemplateRepository;
|
|||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Repository\TimesheetRepository;
|
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
|
use App\Timesheet\DateTimeFactory;
|
||||||
use App\Utils\SearchTerm;
|
use App\Utils\SearchTerm;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Helper\Table;
|
use Symfony\Component\Console\Helper\Table;
|
||||||
@@ -39,10 +39,6 @@ class InvoiceCreateCommand extends Command
|
|||||||
* @var ServiceInvoice
|
* @var ServiceInvoice
|
||||||
*/
|
*/
|
||||||
private $serviceInvoice;
|
private $serviceInvoice;
|
||||||
/**
|
|
||||||
* @var TimesheetRepository
|
|
||||||
*/
|
|
||||||
private $timesheetRepository;
|
|
||||||
/**
|
/**
|
||||||
* @var CustomerRepository
|
* @var CustomerRepository
|
||||||
*/
|
*/
|
||||||
@@ -70,7 +66,6 @@ class InvoiceCreateCommand extends Command
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ServiceInvoice $serviceInvoice,
|
ServiceInvoice $serviceInvoice,
|
||||||
TimesheetRepository $timesheetRepository,
|
|
||||||
CustomerRepository $customerRepository,
|
CustomerRepository $customerRepository,
|
||||||
ProjectRepository $projectRepository,
|
ProjectRepository $projectRepository,
|
||||||
InvoiceTemplateRepository $invoiceTemplateRepository,
|
InvoiceTemplateRepository $invoiceTemplateRepository,
|
||||||
@@ -78,7 +73,6 @@ class InvoiceCreateCommand extends Command
|
|||||||
EventDispatcherInterface $eventDispatcher
|
EventDispatcherInterface $eventDispatcher
|
||||||
) {
|
) {
|
||||||
$this->serviceInvoice = $serviceInvoice;
|
$this->serviceInvoice = $serviceInvoice;
|
||||||
$this->timesheetRepository = $timesheetRepository;
|
|
||||||
$this->customerRepository = $customerRepository;
|
$this->customerRepository = $customerRepository;
|
||||||
$this->projectRepository = $projectRepository;
|
$this->projectRepository = $projectRepository;
|
||||||
$this->invoiceTemplateRepository = $invoiceTemplateRepository;
|
$this->invoiceTemplateRepository = $invoiceTemplateRepository;
|
||||||
@@ -99,7 +93,7 @@ class InvoiceCreateCommand extends Command
|
|||||||
->addOption('user', null, InputOption::VALUE_REQUIRED, 'The user to be used for generating the invoices')
|
->addOption('user', null, InputOption::VALUE_REQUIRED, 'The user to be used for generating the invoices')
|
||||||
->addOption('start', null, InputOption::VALUE_OPTIONAL, 'Start date (format: 2020-01-01, default: start of the month)', null)
|
->addOption('start', null, InputOption::VALUE_OPTIONAL, 'Start date (format: 2020-01-01, default: start of the month)', null)
|
||||||
->addOption('end', null, InputOption::VALUE_OPTIONAL, 'End date (format: 2020-01-31, default: end of the month)', null)
|
->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('timezone', null, InputOption::VALUE_OPTIONAL, 'Timezone for start and end date query (fallback: users timezone)', null)
|
||||||
->addOption('customer', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of customer IDs', null)
|
->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('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-customer', null, InputOption::VALUE_NONE, 'If set, one invoice for each active customer in the given timerange is created')
|
||||||
@@ -157,7 +151,13 @@ class InvoiceCreateCommand extends Command
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$timezone = new \DateTimeZone($input->getOption('timezone'));
|
$timezone = $input->getOption('timezone');
|
||||||
|
if ($timezone === null) {
|
||||||
|
$timezone = $user->getTimezone();
|
||||||
|
}
|
||||||
|
|
||||||
|
$timezone = new \DateTimeZone($timezone);
|
||||||
|
$dateFactory = new DateTimeFactory($timezone);
|
||||||
|
|
||||||
if (!empty($input->getOption('start')) && empty($input->getOption('end'))) {
|
if (!empty($input->getOption('start')) && empty($input->getOption('end'))) {
|
||||||
$io->error('You need to supply a end date if a start date was given');
|
$io->error('You need to supply a end date if a start date was given');
|
||||||
@@ -191,7 +191,7 @@ class InvoiceCreateCommand extends Command
|
|||||||
$start = $input->getOption('start');
|
$start = $input->getOption('start');
|
||||||
if (!empty($start)) {
|
if (!empty($start)) {
|
||||||
try {
|
try {
|
||||||
$start = new \DateTime($start, $timezone);
|
$start = $dateFactory->createDateTime($start);
|
||||||
} catch (\Exception $ex) {
|
} catch (\Exception $ex) {
|
||||||
$io->error('Invalid start date given');
|
$io->error('Invalid start date given');
|
||||||
|
|
||||||
@@ -199,14 +199,14 @@ class InvoiceCreateCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$start instanceof \DateTime) {
|
if (!$start instanceof \DateTime) {
|
||||||
$start = new \DateTime('first day of this month', $timezone);
|
$start = $dateFactory->getStartOfMonth();
|
||||||
}
|
}
|
||||||
$start->setTime(0, 0, 0);
|
$start->setTime(0, 0, 0);
|
||||||
|
|
||||||
$end = $input->getOption('end');
|
$end = $input->getOption('end');
|
||||||
if (!empty($end)) {
|
if (!empty($end)) {
|
||||||
try {
|
try {
|
||||||
$end = new \DateTime($end, $timezone);
|
$end = $dateFactory->createDateTime($end);
|
||||||
} catch (\Exception $ex) {
|
} catch (\Exception $ex) {
|
||||||
$io->error('Invalid end date given');
|
$io->error('Invalid end date given');
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ class InvoiceCreateCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$end instanceof \DateTime) {
|
if (!$end instanceof \DateTime) {
|
||||||
$end = new \DateTime('last day of this month', $timezone);
|
$end = $dateFactory->getEndOfMonth();
|
||||||
}
|
}
|
||||||
$end->setTime(23, 59, 59);
|
$end->setTime(23, 59, 59);
|
||||||
|
|
||||||
@@ -519,7 +519,7 @@ class InvoiceCreateCommand extends Command
|
|||||||
*/
|
*/
|
||||||
private function getActiveCustomers(InvoiceQuery $invoiceQuery): array
|
private function getActiveCustomers(InvoiceQuery $invoiceQuery): array
|
||||||
{
|
{
|
||||||
$results = $this->timesheetRepository->getTimesheetsForQuery($invoiceQuery);
|
$results = $this->serviceInvoice->getInvoiceItems($invoiceQuery);
|
||||||
|
|
||||||
$customers = [];
|
$customers = [];
|
||||||
|
|
||||||
@@ -537,7 +537,7 @@ class InvoiceCreateCommand extends Command
|
|||||||
*/
|
*/
|
||||||
private function getActiveProjects(InvoiceQuery $invoiceQuery): array
|
private function getActiveProjects(InvoiceQuery $invoiceQuery): array
|
||||||
{
|
{
|
||||||
$results = $this->timesheetRepository->getTimesheetsForQuery($invoiceQuery);
|
$results = $this->serviceInvoice->getInvoiceItems($invoiceQuery);
|
||||||
|
|
||||||
$projects = [];
|
$projects = [];
|
||||||
|
|
||||||
|
|||||||
@@ -209,10 +209,8 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
|||||||
$form->submit($submitData, false);
|
$form->submit($submitData, false);
|
||||||
|
|
||||||
if (!$form->isValid()) {
|
if (!$form->isValid()) {
|
||||||
$data->resetByFormError($form->getErrors());
|
$data->resetByFormError($form->getErrors(true));
|
||||||
}
|
} elseif ($request->query->has('setDefaultQuery')) {
|
||||||
|
|
||||||
if ($request->query->has('setDefaultQuery')) {
|
|
||||||
$params = [];
|
$params = [];
|
||||||
foreach ($form->all() as $name => $child) {
|
foreach ($form->all() as $name => $child) {
|
||||||
$params[$name] = $child->getViewData();
|
$params[$name] = $child->getViewData();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Configuration\SystemConfiguration;
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Customer;
|
||||||
use App\Entity\Invoice;
|
use App\Entity\Invoice;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
use App\Export\Spreadsheet\AnnotatedObjectExporter;
|
use App\Export\Spreadsheet\AnnotatedObjectExporter;
|
||||||
@@ -30,7 +31,6 @@ use App\Repository\Query\InvoiceQuery;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\Form\SubmitButton;
|
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -83,45 +83,30 @@ final class InvoiceController extends AbstractController
|
|||||||
$this->flashWarning('invoice.first_template');
|
$this->flashWarning('invoice.first_template');
|
||||||
}
|
}
|
||||||
|
|
||||||
$model = null;
|
|
||||||
|
|
||||||
$query = $this->getDefaultQuery();
|
$query = $this->getDefaultQuery();
|
||||||
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||||
$form->setData($query);
|
if ($this->handleSearch($form, $request)) {
|
||||||
$form->submit($request->query->all(), false);
|
return $this->redirectToRoute('invoice');
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isGranted('create_invoice') && $form->isValid()) {
|
$models = [];
|
||||||
// use the current request locale as fallback, if no translation was configured
|
$total = 0;
|
||||||
if (null !== $query->getTemplate() && null === $query->getTemplate()->getLanguage()) {
|
$searched = false;
|
||||||
$query->getTemplate()->setLanguage($request->getLocale());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if ($form->isValid() && $this->isGranted('create_invoice')) {
|
||||||
/** @var SubmitButton $createButton */
|
if ($request->query->has('createInvoice')) {
|
||||||
$createButton = $form->get('create');
|
|
||||||
if ($createButton->isClicked()) {
|
|
||||||
return $this->renderInvoice($query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var SubmitButton $printButton */
|
|
||||||
$printButton = $form->get('print');
|
|
||||||
if ($printButton->isClicked()) {
|
|
||||||
return $this->service->renderInvoice($query, $this->dispatcher);
|
|
||||||
}
|
|
||||||
} catch (Exception $ex) {
|
|
||||||
$this->logException($ex);
|
|
||||||
$this->flashError('action.update.error', ['%reason%' => 'check doctor/logs']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var SubmitButton $previewButton */
|
|
||||||
$previewButton = $form->get('preview');
|
|
||||||
if ($previewButton->isClicked()) {
|
|
||||||
try {
|
try {
|
||||||
$model = $this->service->createModel($query);
|
return $this->renderInvoice($query, $request);
|
||||||
$entries = $this->service->findInvoiceItems($query);
|
} catch (Exception $ex) {
|
||||||
if (!empty($entries)) {
|
$this->logException($ex);
|
||||||
$model->addEntries($entries);
|
$this->flashError('action.update.error', ['%reason%' => 'check doctor/logs']);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->get('template')->getData() !== null) {
|
||||||
|
try {
|
||||||
|
$models = $this->service->createModels($query);
|
||||||
|
$searched = true;
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
$this->logException($ex);
|
$this->logException($ex);
|
||||||
$this->flashError($ex->getMessage());
|
$this->flashError($ex->getMessage());
|
||||||
@@ -129,14 +114,73 @@ final class InvoiceController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($models as $model) {
|
||||||
|
$total += \count($model->getCalculator()->getEntries());
|
||||||
|
}
|
||||||
|
|
||||||
return $this->render('invoice/index.html.twig', [
|
return $this->render('invoice/index.html.twig', [
|
||||||
'query' => $query,
|
'models' => $models,
|
||||||
'model' => $model,
|
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
|
'limit_preview' => ($total > 500),
|
||||||
|
'searched' => $searched,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getDefaultQuery(): InvoiceQuery
|
/**
|
||||||
|
* @Route(path="/preview/{customer}/{template}", name="invoice_preview", methods={"GET"})
|
||||||
|
* @Security("is_granted('view_invoice')")
|
||||||
|
*/
|
||||||
|
public function previewAction(Customer $customer, InvoiceTemplate $template, Request $request, SystemConfiguration $configuration): Response
|
||||||
|
{
|
||||||
|
if (!$this->templateRepository->hasTemplate()) {
|
||||||
|
return $this->redirectToRoute('invoice');
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->getDefaultQuery();
|
||||||
|
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||||
|
$form->submit($request->query->all(), false);
|
||||||
|
|
||||||
|
if ($form->isValid() && $this->isGranted('create_invoice')) {
|
||||||
|
try {
|
||||||
|
$query->setTemplate($template);
|
||||||
|
$query->setCustomers([$customer]);
|
||||||
|
$model = $this->service->createModel($query);
|
||||||
|
|
||||||
|
return $this->service->renderInvoiceWithModel($model, $this->dispatcher);
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$this->logException($ex);
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => 'Failed generating invoice preview: ' . $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('invoice');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/save-invoice/{customer}/{template}", name="invoice_create", methods={"GET"})
|
||||||
|
* @Security("is_granted('view_invoice')")
|
||||||
|
*/
|
||||||
|
public function createInvoiceAction(Customer $customer, InvoiceTemplate $template, Request $request, SystemConfiguration $configuration): Response
|
||||||
|
{
|
||||||
|
if (!$this->templateRepository->hasTemplate()) {
|
||||||
|
return $this->redirectToRoute('invoice');
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->getDefaultQuery();
|
||||||
|
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||||
|
$form->submit($request->query->all(), false);
|
||||||
|
|
||||||
|
if ($form->isValid() && $this->isGranted('create_invoice')) {
|
||||||
|
$query->setTemplate($template);
|
||||||
|
$query->setCustomers([$customer]);
|
||||||
|
|
||||||
|
return $this->renderInvoice($query, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('invoice');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDefaultQuery(): InvoiceQuery
|
||||||
{
|
{
|
||||||
$factory = $this->getDateTimeFactory();
|
$factory = $this->getDateTimeFactory();
|
||||||
$begin = $factory->getStartOfMonth();
|
$begin = $factory->getStartOfMonth();
|
||||||
@@ -159,20 +203,23 @@ final class InvoiceController extends AbstractController
|
|||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function renderInvoice(InvoiceQuery $query)
|
private function renderInvoice(InvoiceQuery $query, Request $request)
|
||||||
{
|
{
|
||||||
|
// use the current request locale as fallback, if no translation was configured
|
||||||
|
if (null !== $query->getTemplate() && null === $query->getTemplate()->getLanguage()) {
|
||||||
|
$query->getTemplate()->setLanguage($request->getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$invoice = $this->service->createInvoice($query, $this->dispatcher);
|
$invoices = $this->service->createInvoices($query, $this->dispatcher);
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
if ($this->isGranted('history_invoice')) {
|
if (\count($invoices) === 1) {
|
||||||
return $this->redirectToRoute('admin_invoice_list', ['id' => $invoice->getId()]);
|
return $this->redirectToRoute('admin_invoice_list', ['id' => $invoices[0]->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$file = $this->service->getInvoiceFile($invoice);
|
return $this->redirectToRoute('admin_invoice_list');
|
||||||
|
|
||||||
return $this->file($file->getRealPath(), $file->getBasename());
|
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
$this->flashUpdateException($ex);
|
$this->flashUpdateException($ex);
|
||||||
}
|
}
|
||||||
@@ -182,7 +229,6 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/change-status/{id}/{status}", name="admin_invoice_status", methods={"GET"})
|
* @Route(path="/change-status/{id}/{status}", name="admin_invoice_status", methods={"GET"})
|
||||||
* @Security("is_granted('history_invoice')")
|
|
||||||
*/
|
*/
|
||||||
public function changeStatusAction(Invoice $invoice, string $status): Response
|
public function changeStatusAction(Invoice $invoice, string $status): Response
|
||||||
{
|
{
|
||||||
@@ -198,7 +244,6 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/delete/{id}", name="admin_invoice_delete", methods={"GET"})
|
* @Route(path="/delete/{id}", name="admin_invoice_delete", methods={"GET"})
|
||||||
* @Security("is_granted('history_invoice')")
|
|
||||||
*/
|
*/
|
||||||
public function deleteInvoiceAction(Invoice $invoice): Response
|
public function deleteInvoiceAction(Invoice $invoice): Response
|
||||||
{
|
{
|
||||||
@@ -214,7 +259,6 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/download/{id}", name="admin_invoice_download", methods={"GET"})
|
* @Route(path="/download/{id}", name="admin_invoice_download", methods={"GET"})
|
||||||
* @Security("is_granted('history_invoice')")
|
|
||||||
*/
|
*/
|
||||||
public function downloadAction(Invoice $invoice): Response
|
public function downloadAction(Invoice $invoice): Response
|
||||||
{
|
{
|
||||||
@@ -231,7 +275,6 @@ final class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/show/{page}", defaults={"page": 1}, requirements={"page": "[1-9]\d*"}, name="admin_invoice_list", methods={"GET"})
|
* @Route(path="/show/{page}", defaults={"page": 1}, requirements={"page": "[1-9]\d*"}, name="admin_invoice_list", methods={"GET"})
|
||||||
* @Security("is_granted('history_invoice')")
|
|
||||||
*/
|
*/
|
||||||
public function showInvoicesAction(Request $request, int $page): Response
|
public function showInvoicesAction(Request $request, int $page): Response
|
||||||
{
|
{
|
||||||
@@ -380,10 +423,6 @@ final class InvoiceController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom): Response
|
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom): Response
|
||||||
{
|
{
|
||||||
if (!$this->templateRepository->hasTemplate()) {
|
|
||||||
$this->flashWarning('invoice.first_template');
|
|
||||||
}
|
|
||||||
|
|
||||||
$template = new InvoiceTemplate();
|
$template = new InvoiceTemplate();
|
||||||
|
|
||||||
if (null !== $copyFrom) {
|
if (null !== $copyFrom) {
|
||||||
@@ -410,7 +449,7 @@ final class InvoiceController extends AbstractController
|
|||||||
return $this->redirectToRoute('admin_invoice_template');
|
return $this->redirectToRoute('admin_invoice_template');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
private function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
||||||
{
|
{
|
||||||
$editForm = $this->createEditForm($template);
|
$editForm = $this->createEditForm($template);
|
||||||
|
|
||||||
@@ -433,7 +472,7 @@ final class InvoiceController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getToolbarForm(InvoiceQuery $query, bool $simple): FormInterface
|
private function getToolbarForm(InvoiceQuery $query, bool $simple): FormInterface
|
||||||
{
|
{
|
||||||
$form = $simple ? InvoiceToolbarSimpleForm::class : InvoiceToolbarForm::class;
|
$form = $simple ? InvoiceToolbarSimpleForm::class : InvoiceToolbarForm::class;
|
||||||
|
|
||||||
|
|||||||
@@ -30,15 +30,13 @@ class InvoiceSubscriber extends AbstractActionsSubscriber
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isGranted('history_invoice')) {
|
if ($invoice->isNew()) {
|
||||||
if ($invoice->isNew()) {
|
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending'])]);
|
||||||
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending'])]);
|
} elseif ($invoice->isPending()) {
|
||||||
} elseif ($invoice->isPending()) {
|
$event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid'])]);
|
||||||
$event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid'])]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
|
|
||||||
$event->addDelete($this->path('admin_invoice_delete', ['id' => $invoice->getId()]), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
|
||||||
|
$event->addDelete($this->path('admin_invoice_delete', ['id' => $invoice->getId()]), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ class InvoicesSubscriber extends AbstractActionsSubscriber
|
|||||||
{
|
{
|
||||||
$event->addColumnToggle('#modal_invoice');
|
$event->addColumnToggle('#modal_invoice');
|
||||||
|
|
||||||
if ($this->isGranted('history_invoice')) {
|
$event->addAction('list', ['url' => $this->path('admin_invoice_list')]);
|
||||||
$event->addAction('list', ['url' => $this->path('admin_invoice_list')]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->isGranted('manage_invoice_template')) {
|
if ($this->isGranted('manage_invoice_template')) {
|
||||||
$event->addAction('invoice-template', ['url' => $this->path('admin_invoice_template')]);
|
$event->addAction('invoice-template', ['url' => $this->path('admin_invoice_template')]);
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
|||||||
|
|
||||||
$extendedOptions = ['class' => 'selectpicker'];
|
$extendedOptions = ['class' => 'selectpicker'];
|
||||||
|
|
||||||
|
if ($options['multiple']) {
|
||||||
|
$extendedOptions['size'] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (false !== $options['width']) {
|
if (false !== $options['width']) {
|
||||||
$extendedOptions['data-width'] = $options['width'];
|
$extendedOptions['data-width'] = $options['width'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ namespace App\Form\Toolbar;
|
|||||||
use App\Form\Type\InvoiceTemplateType;
|
use App\Form\Type\InvoiceTemplateType;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
@@ -28,22 +27,12 @@ class InvoiceToolbarSimpleForm extends AbstractToolbarForm
|
|||||||
{
|
{
|
||||||
$this->addTemplateChoice($builder);
|
$this->addTemplateChoice($builder);
|
||||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||||
$this->addCustomerChoice($builder, ['required' => true, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
|
$this->addCustomerMultiChoice($builder, ['required' => false, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
|
||||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], false, true);
|
$this->addProjectMultiChoice($builder, ['ignore_date' => true], false, true);
|
||||||
$builder->add('markAsExported', CheckboxType::class, [
|
$builder->add('markAsExported', CheckboxType::class, [
|
||||||
'label' => 'label.mark_as_exported',
|
'label' => 'label.mark_as_exported',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
$builder->add('create', SubmitType::class, [
|
|
||||||
'label' => 'action.save',
|
|
||||||
]);
|
|
||||||
$builder->add('print', SubmitType::class, [
|
|
||||||
'label' => 'button.preview_print',
|
|
||||||
'attr' => ['formtarget' => '_blank'],
|
|
||||||
]);
|
|
||||||
$builder->add('preview', SubmitType::class, [
|
|
||||||
'label' => 'button.preview',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addTemplateChoice(FormBuilderInterface $builder)
|
protected function addTemplateChoice(FormBuilderInterface $builder)
|
||||||
|
|||||||
@@ -21,21 +21,9 @@ class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
|||||||
$total = $model->getCalculator()->getTotal();
|
$total = $model->getCalculator()->getTotal();
|
||||||
$subtotal = $model->getCalculator()->getSubtotal();
|
$subtotal = $model->getCalculator()->getSubtotal();
|
||||||
$formatter = $model->getFormatter();
|
$formatter = $model->getFormatter();
|
||||||
$entries = $model->getCalculator()->getEntries();
|
|
||||||
|
|
||||||
$begin = null;
|
$begin = $model->getQuery()->getBegin();
|
||||||
if ($model->getQuery()->getBegin() !== null) {
|
$end = $model->getQuery()->getEnd();
|
||||||
$begin = $model->getQuery()->getBegin();
|
|
||||||
} elseif (!empty($entries)) {
|
|
||||||
$begin = $entries[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
$end = null;
|
|
||||||
if ($model->getQuery()->getEnd() !== null) {
|
|
||||||
$end = $model->getQuery()->getEnd();
|
|
||||||
} elseif (!empty($entries)) {
|
|
||||||
$end = array_keys($entries)[\count($entries) - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
$values = [
|
$values = [
|
||||||
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
|
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ final class ServiceInvoice
|
|||||||
{
|
{
|
||||||
foreach ($this->getNumberGenerator() as $generator) {
|
foreach ($this->getNumberGenerator() as $generator) {
|
||||||
if ($generator->getId() === $name) {
|
if ($generator->getId() === $name) {
|
||||||
return $generator;
|
// several models can co-exist at the same time and NumberGeneratorInterface works
|
||||||
|
// with setModel() instead of __construct() - returning the same instance would lead to bugs!
|
||||||
|
return clone $generator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +118,9 @@ final class ServiceInvoice
|
|||||||
{
|
{
|
||||||
foreach ($this->getCalculator() as $calculator) {
|
foreach ($this->getCalculator() as $calculator) {
|
||||||
if ($calculator->getId() === $name) {
|
if ($calculator->getId() === $name) {
|
||||||
return $calculator;
|
// several models can co-exist at the same time and CalculatorInterface works
|
||||||
|
// with setModel() instead of __construct() - returning the same instance would lead to bugs!
|
||||||
|
return clone $calculator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,50 +262,33 @@ final class ServiceInvoice
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param InvoiceQuery $query
|
* @param InvoiceQuery $query
|
||||||
* @return array<string, InvoiceItemInterface[]>
|
* @return InvoiceItemInterface[]
|
||||||
*/
|
*/
|
||||||
private function findInvoiceItemsWithRepository(InvoiceQuery $query): array
|
public function findInvoiceItems(InvoiceQuery $query): array
|
||||||
{
|
{
|
||||||
|
@trigger_error('Using findInvoiceItems() is deprecated since 1.14 and will be removed with 2.0', E_USER_DEPRECATED);
|
||||||
|
|
||||||
// customer needs to be defined, as we need the currency for the invoice
|
// customer needs to be defined, as we need the currency for the invoice
|
||||||
if (!$query->hasCustomers()) {
|
if (!$query->hasCustomers()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$factory = $this->getDateTimeFactory($query);
|
return $this->getInvoiceItems($query);
|
||||||
|
|
||||||
if (null === $query->getBegin()) {
|
|
||||||
$query->setBegin($factory->getStartOfMonth());
|
|
||||||
}
|
|
||||||
if (null === $query->getEnd()) {
|
|
||||||
$query->setEnd($factory->getEndOfMonth());
|
|
||||||
}
|
|
||||||
$query->getBegin()->setTime(0, 0, 0);
|
|
||||||
$query->getEnd()->setTime(23, 59, 59);
|
|
||||||
|
|
||||||
$repositories = $this->getInvoiceItemRepositories();
|
|
||||||
$items = [];
|
|
||||||
|
|
||||||
foreach ($repositories as $repository) {
|
|
||||||
$items[\get_class($repository)] = $repository->getInvoiceItemsForQuery($query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param InvoiceQuery $query
|
* @param InvoiceQuery $query
|
||||||
* @return InvoiceItemInterface[]
|
* @return InvoiceItemInterface[]
|
||||||
*/
|
*/
|
||||||
public function findInvoiceItems(InvoiceQuery $query): array
|
public function getInvoiceItems(InvoiceQuery $query): array
|
||||||
{
|
{
|
||||||
$entries = [];
|
$items = [];
|
||||||
$temp = $this->findInvoiceItemsWithRepository($query);
|
|
||||||
|
|
||||||
foreach ($temp as $repo => $items) {
|
foreach ($this->getInvoiceItemRepositories() as $repository) {
|
||||||
$entries = array_merge($entries, $items);
|
$items = array_merge($items, $repository->getInvoiceItemsForQuery($query));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $entries;
|
return $items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getDateTimeFactory(InvoiceQuery $query): DateTimeFactory
|
private function getDateTimeFactory(InvoiceQuery $query): DateTimeFactory
|
||||||
@@ -316,29 +303,17 @@ final class ServiceInvoice
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, InvoiceItemInterface[]> $entries
|
* @param InvoiceItemInterface[] $entries
|
||||||
*/
|
*/
|
||||||
private function markEntriesAsExported(iterable $entries)
|
private function markEntriesAsExported(array $entries)
|
||||||
{
|
{
|
||||||
$repositories = $this->getInvoiceItemRepositories();
|
foreach ($this->getInvoiceItemRepositories() as $repository) {
|
||||||
|
$repository->setExported($entries);
|
||||||
foreach ($entries as $repo => $items) {
|
|
||||||
foreach ($repositories as $repository) {
|
|
||||||
if (\get_class($repository) === $repo) {
|
|
||||||
$repository->setExported($items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function renderInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Response
|
public function renderInvoiceWithModel(InvoiceModel $model, EventDispatcherInterface $dispatcher): Response
|
||||||
{
|
{
|
||||||
$entries = $this->findInvoiceItemsWithRepository($query);
|
|
||||||
$model = $this->createModel($query);
|
|
||||||
foreach ($entries as $repo => $items) {
|
|
||||||
$model->addEntries($items);
|
|
||||||
}
|
|
||||||
|
|
||||||
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
|
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
|
||||||
if (null === $document) {
|
if (null === $document) {
|
||||||
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
|
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
|
||||||
@@ -361,20 +336,21 @@ final class ServiceInvoice
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function renderInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Response
|
||||||
|
{
|
||||||
|
$model = $this->createModel($query);
|
||||||
|
|
||||||
|
return $this->renderInvoiceWithModel($model, $dispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param InvoiceQuery $query
|
* @param InvoiceModel $model
|
||||||
* @param EventDispatcherInterface $dispatcher
|
* @param EventDispatcherInterface $dispatcher
|
||||||
* @return Invoice
|
* @return Invoice
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function createInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Invoice
|
public function createInvoiceFromModel(InvoiceModel $model, EventDispatcherInterface $dispatcher): Invoice
|
||||||
{
|
{
|
||||||
$entries = $this->findInvoiceItemsWithRepository($query);
|
|
||||||
$model = $this->createModel($query);
|
|
||||||
foreach ($entries as $repo => $items) {
|
|
||||||
$model->addEntries($items);
|
|
||||||
}
|
|
||||||
|
|
||||||
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
|
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
|
||||||
if (null === $document) {
|
if (null === $document) {
|
||||||
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
|
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
|
||||||
@@ -386,8 +362,8 @@ final class ServiceInvoice
|
|||||||
|
|
||||||
$response = $renderer->render($document, $model);
|
$response = $renderer->render($document, $model);
|
||||||
|
|
||||||
if ($query->isMarkAsExported()) {
|
if ($model->getQuery()->isMarkAsExported()) {
|
||||||
$this->markEntriesAsExported($entries);
|
$this->markEntriesAsExported($model->getEntries());
|
||||||
}
|
}
|
||||||
|
|
||||||
$event = new InvoicePostRenderEvent($model, $document, $renderer, $response);
|
$event = new InvoicePostRenderEvent($model, $document, $renderer, $response);
|
||||||
@@ -411,6 +387,37 @@ final class ServiceInvoice
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param InvoiceQuery $query
|
||||||
|
* @param EventDispatcherInterface $dispatcher
|
||||||
|
* @return Invoice[]
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function createInvoices(InvoiceQuery $query, EventDispatcherInterface $dispatcher): array
|
||||||
|
{
|
||||||
|
$invoices = [];
|
||||||
|
|
||||||
|
$models = $this->createModels($query);
|
||||||
|
foreach ($models as $model) {
|
||||||
|
$invoices[] = $this->createInvoiceFromModel($model, $dispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $invoices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param InvoiceQuery $query
|
||||||
|
* @param EventDispatcherInterface $dispatcher
|
||||||
|
* @return Invoice
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function createInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Invoice
|
||||||
|
{
|
||||||
|
$model = $this->createModel($query);
|
||||||
|
|
||||||
|
return $this->createInvoiceFromModel($model, $dispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
public function deleteInvoice(Invoice $invoice)
|
public function deleteInvoice(Invoice $invoice)
|
||||||
{
|
{
|
||||||
$invoiceDirectory = $this->getInvoicesDirectory();
|
$invoiceDirectory = $this->getInvoicesDirectory();
|
||||||
@@ -426,9 +433,23 @@ final class ServiceInvoice
|
|||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function createModel(InvoiceQuery $query): InvoiceModel
|
public function createModel(InvoiceQuery $query): InvoiceModel
|
||||||
|
{
|
||||||
|
$model = $this->createModelWithoutEntries($query);
|
||||||
|
$model->addEntries($this->getInvoiceItems($query));
|
||||||
|
|
||||||
|
$this->prepareModelQueryDates($model);
|
||||||
|
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createModelWithoutEntries(InvoiceQuery $query): InvoiceModel
|
||||||
{
|
{
|
||||||
$template = $query->getTemplate();
|
$template = $query->getTemplate();
|
||||||
|
|
||||||
|
if (!$query->hasCustomers()) {
|
||||||
|
throw new \Exception('Cannot create invoice model without customer');
|
||||||
|
}
|
||||||
|
|
||||||
if (null === $template) {
|
if (null === $template) {
|
||||||
throw new \Exception('Cannot create invoice model without template');
|
throw new \Exception('Cannot create invoice model without template');
|
||||||
}
|
}
|
||||||
@@ -449,9 +470,7 @@ final class ServiceInvoice
|
|||||||
$model->setUser($query->getCurrentUser());
|
$model->setUser($query->getCurrentUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($query->hasCustomers()) {
|
$model->setCustomer($query->getCustomers()[0]);
|
||||||
$model->setCustomer($query->getCustomers()[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$generator = $this->getNumberGeneratorByName($query->getTemplate()->getNumberGenerator());
|
$generator = $this->getNumberGeneratorByName($query->getTemplate()->getNumberGenerator());
|
||||||
if (null === $generator) {
|
if (null === $generator) {
|
||||||
@@ -468,4 +487,95 @@ final class ServiceInvoice
|
|||||||
|
|
||||||
return $model;
|
return $model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function prepareModelQueryDates(InvoiceModel $model)
|
||||||
|
{
|
||||||
|
$begin = $model->getQuery()->getBegin();
|
||||||
|
$end = $model->getQuery()->getEnd();
|
||||||
|
|
||||||
|
if ($begin !== null && $end !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\count($model->getEntries()) === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmpBegin = null;
|
||||||
|
$tmpEnd = null;
|
||||||
|
|
||||||
|
foreach ($model->getEntries() as $entry) {
|
||||||
|
if ($begin === null) {
|
||||||
|
if ($tmpBegin === null) {
|
||||||
|
$tmpBegin = $entry->getBegin();
|
||||||
|
} else {
|
||||||
|
$tmpBegin = min($entry->getBegin(), $tmpBegin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end === null) {
|
||||||
|
if ($tmpEnd === null) {
|
||||||
|
$tmpEnd = $entry->getEnd();
|
||||||
|
} else {
|
||||||
|
$tmpEnd = max($entry->getEnd(), $tmpEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($begin === null && $tmpBegin !== null) {
|
||||||
|
$model->getQuery()->setBegin($tmpBegin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end === null && $tmpEnd !== null) {
|
||||||
|
$model->getQuery()->setEnd($tmpEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param InvoiceQuery $query
|
||||||
|
* @return InvoiceModel[]
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function createModels(InvoiceQuery $query): array
|
||||||
|
{
|
||||||
|
$models = [];
|
||||||
|
$customerEntries = [];
|
||||||
|
$items = $this->getInvoiceItems($query);
|
||||||
|
|
||||||
|
foreach ($items as $entry) {
|
||||||
|
$customer = $entry->getProject()->getCustomer();
|
||||||
|
$id = $customer->getId();
|
||||||
|
if (!\array_key_exists($id, $customerEntries)) {
|
||||||
|
$customerEntries[$id] = [
|
||||||
|
'customer' => $customer,
|
||||||
|
'entries' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$customerEntries[$id]['entries'][] = $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($customerEntries)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
uasort($customerEntries, function ($a, $b) {
|
||||||
|
return strcmp($a['customer']->getName(), $b['customer']->getName());
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($customerEntries as $id => $settings) {
|
||||||
|
if (empty($settings['entries'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customerQuery = clone $query;
|
||||||
|
$customerQuery->setCustomers([$settings['customer']]);
|
||||||
|
$model = $this->createModelWithoutEntries($customerQuery);
|
||||||
|
$model->addEntries($settings['entries']);
|
||||||
|
$this->prepareModelQueryDates($model);
|
||||||
|
|
||||||
|
$models[] = $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $models;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class TimesheetInvoiceItemRepository implements InvoiceItemRepositoryInter
|
|||||||
*/
|
*/
|
||||||
public function getInvoiceItemsForQuery(InvoiceQuery $query): iterable
|
public function getInvoiceItemsForQuery(InvoiceQuery $query): iterable
|
||||||
{
|
{
|
||||||
return $this->repository->getTimesheetsForQuery($query);
|
return $this->repository->getTimesheetsForQuery($query, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use App\Repository\Paginator\PaginatorInterface;
|
|||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use DateInterval;
|
use DateInterval;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\AbstractQuery;
|
use Doctrine\ORM\AbstractQuery;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
@@ -34,7 +35,6 @@ use Doctrine\ORM\QueryBuilder;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Pagerfanta\Pagerfanta;
|
use Pagerfanta\Pagerfanta;
|
||||||
use PDO;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Doctrine\ORM\EntityRepository<Timesheet>
|
* @extends \Doctrine\ORM\EntityRepository<Timesheet>
|
||||||
@@ -837,15 +837,15 @@ class TimesheetRepository extends EntityRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($query->isExported()) {
|
if ($query->isExported()) {
|
||||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', true, PDO::PARAM_BOOL);
|
$qb->andWhere('t.exported = :exported')->setParameter('exported', true, Types::BOOLEAN);
|
||||||
} elseif ($query->isNotExported()) {
|
} elseif ($query->isNotExported()) {
|
||||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', false, PDO::PARAM_BOOL);
|
$qb->andWhere('t.exported = :exported')->setParameter('exported', false, Types::BOOLEAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($query->isBillable()) {
|
if ($query->isBillable()) {
|
||||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', true, PDO::PARAM_BOOL);
|
$qb->andWhere('t.billable = :billable')->setParameter('billable', true, Types::BOOLEAN);
|
||||||
} elseif ($query->isNotBillable()) {
|
} elseif ($query->isNotBillable()) {
|
||||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', false, PDO::PARAM_BOOL);
|
$qb->andWhere('t.billable = :billable')->setParameter('billable', false, Types::BOOLEAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $query->getModifiedAfter()) {
|
if (null !== $query->getModifiedAfter()) {
|
||||||
@@ -942,7 +942,7 @@ class TimesheetRepository extends EntityRepository
|
|||||||
->groupBy('a.id', 'p.id')
|
->groupBy('a.id', 'p.id')
|
||||||
->orderBy('maxid', 'DESC')
|
->orderBy('maxid', 'DESC')
|
||||||
->setMaxResults($limit)
|
->setMaxResults($limit)
|
||||||
->setParameter('visible', true, PDO::PARAM_BOOL)
|
->setParameter('visible', true, Types::BOOLEAN)
|
||||||
;
|
;
|
||||||
|
|
||||||
if (null !== $user) {
|
if (null !== $user) {
|
||||||
@@ -974,7 +974,7 @@ class TimesheetRepository extends EntityRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet[] $timesheets
|
* @param Timesheet[]|int[] $timesheets
|
||||||
*/
|
*/
|
||||||
public function setExported(array $timesheets)
|
public function setExported(array $timesheets)
|
||||||
{
|
{
|
||||||
@@ -987,7 +987,7 @@ class TimesheetRepository extends EntityRepository
|
|||||||
->update(Timesheet::class, 't')
|
->update(Timesheet::class, 't')
|
||||||
->set('t.exported', ':exported')
|
->set('t.exported', ':exported')
|
||||||
->where($qb->expr()->in('t.id', ':ids'))
|
->where($qb->expr()->in('t.id', ':ids'))
|
||||||
->setParameter('exported', true, PDO::PARAM_BOOL)
|
->setParameter('exported', true, Types::BOOLEAN)
|
||||||
->setParameter('ids', $timesheets)
|
->setParameter('ids', $timesheets)
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->execute();
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
|
||||||
{% import "macros/datatables.html.twig" as tables %}
|
{% import "macros/datatables.html.twig" as tables %}
|
||||||
|
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||||
{% import "invoice/actions.html.twig" as actions %}
|
{% import "invoice/actions.html.twig" as actions %}
|
||||||
|
|
||||||
{% set columns = {
|
{% set columns = {
|
||||||
'date': {'class': 'alwaysVisible', 'orderBy': false},
|
'date': {'class': 'alwaysVisible w-min', 'orderBy': false},
|
||||||
'user': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
|
||||||
'project': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
'project': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
||||||
'description': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
'description': {'class': 'hidden-xs hidden-sm hidden', 'orderBy': false},
|
||||||
'unit_price': {'class': 'hidden-xs text-center', 'orderBy': false},
|
'user': {'class': 'hidden-xs hidden-sm w-min', 'orderBy': false},
|
||||||
'amount': {'class': 'text-center', 'orderBy': false},
|
'unit_price': {'class': 'hidden-xs text-center w-min', 'orderBy': false},
|
||||||
'duration': {'class': 'hidden-xs text-center', 'orderBy': false},
|
'amount': {'class': 'text-center w-min', 'orderBy': false},
|
||||||
'total_rate': {'class': 'text-right alwaysVisible', 'orderBy': false},
|
'duration': {'class': 'hidden-xs text-center w-min', 'orderBy': false},
|
||||||
|
'total_rate': {'class': 'text-right alwaysVisible w-min', 'orderBy': false},
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
{% set tableName = 'invoice' %}
|
{% set tableName = 'invoice' %}
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
{% if is_granted('create_invoice') %}
|
{% if is_granted('create_invoice') %}
|
||||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||||
|
{% import "macros/search.html.twig" as search %}
|
||||||
{% form_theme form '@AdminLTE/layout/form-theme-horizontal.html.twig' %}
|
{% form_theme form '@AdminLTE/layout/form-theme-horizontal.html.twig' %}
|
||||||
{% block box_title %}{{ 'invoice.filter'|trans }}{% endblock %}
|
{% block box_title %}{{ 'invoice.filter'|trans }}{% endblock %}
|
||||||
{% block box_before %}{{ form_start(form) }}{% endblock %}
|
{% block box_before %}{{ form_start(form) }}{% endblock %}
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
{{ form_row(form.searchTerm) }}
|
{{ form_row(form.searchTerm) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ form_row(form.daterange) }}
|
{{ form_row(form.daterange) }}
|
||||||
{{ form_row(form.customer) }}
|
{{ form_row(form.customers) }}
|
||||||
{{ form_row(form.projects) }}
|
{{ form_row(form.projects) }}
|
||||||
{% if form.activities is defined %}
|
{% if form.activities is defined %}
|
||||||
{{ form_row(form.activities) }}
|
{{ form_row(form.activities) }}
|
||||||
@@ -55,13 +56,7 @@
|
|||||||
{{ form_row(form.markAsExported) }}
|
{{ form_row(form.markAsExported) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block box_footer%}
|
{% block box_footer%}
|
||||||
{% set createAttr = {'class': 'btn btn-success'} %}
|
{{ search.searchButton(form) }}
|
||||||
{% if not is_granted('history_invoice') %}
|
|
||||||
{% set createAttr = createAttr|merge({'formtarget': '_blank'}) %}
|
|
||||||
{% endif %}
|
|
||||||
{{ form_widget(form.create, {'attr': createAttr}) }}
|
|
||||||
{{ form_widget(form.print) }}
|
|
||||||
{{ form_widget(form.preview) }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block box_after %}{{ form_end(form) }}{% endblock %}
|
{% block box_after %}{{ form_end(form) }}{% endblock %}
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
@@ -69,66 +64,177 @@
|
|||||||
{{ widgets.callout('danger', 'http_error_403.suggestion'|trans({}, 'exceptions')) }}
|
{{ widgets.callout('danger', 'http_error_403.suggestion'|trans({}, 'exceptions')) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if model is not null %}
|
{% if searched %}
|
||||||
{% if model.calculator is empty or model.calculator.entries is empty %}
|
{% set showEmpty = true %}
|
||||||
|
{% for model in models %}
|
||||||
|
{% if showEmpty %}
|
||||||
|
{% set showEmpty = model.calculator is empty or model.calculator.entries is empty %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if showEmpty %}
|
||||||
{{ widgets.nothing_found() }}
|
{{ widgets.nothing_found() }}
|
||||||
{% else %}
|
|
||||||
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
|
||||||
{% set entries = model.calculator.entries %}
|
|
||||||
{% set currency = model.currency %}
|
|
||||||
{{ tables.datatable_header(tableName, columns, query, {}) }}
|
|
||||||
{% for entry in entries %}
|
|
||||||
{% set amount = entry.amount %}
|
|
||||||
{% set duration = entry.duration|duration(isDecimal) %}
|
|
||||||
{% set rate = 0 %}
|
|
||||||
{% if entry.fixedRate is not null %}
|
|
||||||
{% set rate = entry.fixedRate %}
|
|
||||||
{% elseif entry.hourlyRate is not null %}
|
|
||||||
{% set rate = entry.hourlyRate %}
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
|
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
|
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
|
||||||
{{ widgets.label_project(entry.project) }}
|
|
||||||
{% if entry.activity is not null %}
|
|
||||||
<br>
|
|
||||||
<small>
|
|
||||||
{{ widgets.label_activity(entry.activity) }}
|
|
||||||
</small>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
|
||||||
{% if entry.description is not empty %}
|
|
||||||
{{ entry.description|desc2html }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-center">{{ rate|money(currency) }}</td>
|
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }} text-center text-nowrap">{{ amount }}</td>
|
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }} text-center text-nowrap" data-duration="{{ entry.duration }}">{{ duration }}</td>
|
|
||||||
<td class="text-right text-nowrap">{{ entry.rate|money(currency) }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th class="{{ tables.data_table_column_class(tableName, columns, 'user') }}"></th>
|
|
||||||
<th class="{{ tables.data_table_column_class(tableName, columns, 'project') }}"></th>
|
|
||||||
<th class="{{ tables.data_table_column_class(tableName, columns, 'description') }}"></th>
|
|
||||||
<th class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}"></th>
|
|
||||||
<th class="{{ tables.data_table_column_class(tableName, columns, 'amount') }}"></th>
|
|
||||||
<th class="{{ tables.data_table_column_class(tableName, columns, 'duration') }} text-center text-nowrap">{{ model.calculator.timeWorked|duration(isDecimal) }}</th>
|
|
||||||
<th class="text-right text-nowrap">{{ model.calculator.total|money(currency) }}</th>
|
|
||||||
</tr>
|
|
||||||
{{ tables.data_table_footer(entries) }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if models|length > 0 %}
|
||||||
|
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% import '@AdminLTE/Macros/buttons.html.twig' as button %}
|
||||||
|
{% block box_title %}
|
||||||
|
{{ 'button.preview'|trans }}: {{ 'invoice.title'|trans }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block box_body_class %}no-padding{% endblock %}
|
||||||
|
{% block box_footer %}
|
||||||
|
<a href="#" onclick="return saveAllInvoices(this);" class="btn btn-primary" id="create_all_invoices">
|
||||||
|
{{ 'action.save_all'|trans }}
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
|
{% block box_body %}
|
||||||
|
<table class="table table-striped table-hover dataTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'label.customer'|trans }}</th>
|
||||||
|
<th class="w-min text-center actions"></th>
|
||||||
|
<th class="w-min text-center hidden-xs">{{ 'label.duration'|trans }}</th>
|
||||||
|
<th class="w-min text-right hidden-xs">{{ 'label.total_rate'|trans }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for model in models %}
|
||||||
|
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||||
|
{% set currency = model.currency %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ widgets.label_customer(model.customer) }}
|
||||||
|
</td>
|
||||||
|
<td class="w-min text-center">
|
||||||
|
{{ widgets.action_button('show', {'url': '#invoice_preview_details_' ~ model.customer.id, 'title': 'timesheet.all'|trans, 'class': 'btn btn-sm hidden-xs hidden-sm'}, 'link') }}
|
||||||
|
{{ widgets.action_button('print', {'url': '#', 'onclick': 'return singleInvoice(this)', 'title': 'button.preview'|trans, 'target': '_blank', 'class': 'btn btn-sm', 'attr': {'data-href': path('invoice_preview', {'customer': model.customer.id, 'template': model.template.id})}}, 'success') }}
|
||||||
|
{{ widgets.action_button('save', {'url': '#', 'onclick': 'return singleInvoice(this)', 'title': 'action.save'|trans, 'class': 'btn btn-sm', 'attr': {'data-href': path('invoice_create', {'customer': model.customer.id, 'template': model.template.id})}}, 'primary') }}
|
||||||
|
</td>
|
||||||
|
<td class="w-min text-center hidden-xs">
|
||||||
|
{{ model.calculator.timeWorked|duration(isDecimal) }}
|
||||||
|
</td>
|
||||||
|
<td class="w-min text-right hidden-xs">
|
||||||
|
<span title="{{ 'label.vat'|trans }}: {{ model.calculator.tax|money(currency) }}" data-toggle="tooltip">{{ model.calculator.total|money(currency) }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
{% endembed %}
|
||||||
|
|
||||||
|
{% for model in models %}
|
||||||
|
{% set isEmptyModel = model.calculator is empty or model.calculator.entries is empty %}
|
||||||
|
{% if not isEmptyModel %}
|
||||||
|
{% set customer = model.query.customers[0] %}
|
||||||
|
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||||
|
{% set entries = model.calculator.entries %}
|
||||||
|
{% set currency = model.currency %}
|
||||||
|
|
||||||
|
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% import "macros/datatables.html.twig" as tables %}
|
||||||
|
{% block box_title %}
|
||||||
|
<span id="invoice_preview_details_{{ customer.id }}">{{ widgets.label_customer(customer) }}</span>
|
||||||
|
<small>{{ 'label.duration'|trans }}: {{ model.calculator.timeWorked|duration() }}, {{ 'label.total_rate'|trans }}: {{ model.calculator.total|money(currency) }}</small>
|
||||||
|
{% endblock %}
|
||||||
|
{% block box_body_class %}invoice-preview-box no-padding{% endblock %}
|
||||||
|
{% block box_body %}
|
||||||
|
{{ tables.datatable_header(tableName, columns, model.query, {'boxClass': ''}) }}
|
||||||
|
{% set itemsAmount = entries|length %}
|
||||||
|
{% if limit_preview %}
|
||||||
|
{% set entries = entries|slice(0, 100) %}
|
||||||
|
{% endif %}
|
||||||
|
{% for entry in entries %}
|
||||||
|
{% set amount = entry.amount %}
|
||||||
|
{% set duration = entry.duration|duration(isDecimal) %}
|
||||||
|
{% set rate = 0 %}
|
||||||
|
{% if entry.fixedRate is not null %}
|
||||||
|
{% set rate = entry.fixedRate %}
|
||||||
|
{% elseif entry.hourlyRate is not null %}
|
||||||
|
{% set rate = entry.hourlyRate %}
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||||
|
{{ widgets.label_project(entry.project) }}
|
||||||
|
{% if entry.activity is not null %}
|
||||||
|
<br>
|
||||||
|
<small>
|
||||||
|
{{ widgets.label_activity(entry.activity) }}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
||||||
|
{% if entry.description is not empty %}
|
||||||
|
{{ entry.description|desc2html }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}">{{ rate|money(currency) }}</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }}">{{ amount }}</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}" data-duration="{{ entry.duration }}">{{ duration }}</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">{{ entry.rate|money(currency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% if limit_preview and itemsAmount > 100 %}
|
||||||
|
<tr class="warning">
|
||||||
|
<td colspan="8">» {{ 'preview.skipped_rows'|trans({'%rows%': (itemsAmount - 100)}) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr class="summary">
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}"></td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}"></td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}"></td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}"></td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}"></td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }}"></td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ model.calculator.timeWorked|duration(isDecimal) }}</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">{{ model.calculator.total|money(currency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{{ tables.data_table_footer(entries) }}
|
||||||
|
{% endblock %}
|
||||||
|
{% endembed %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
|
{% set formId = form.vars.attr.id %}
|
||||||
|
{% set formSelector = 'form#' ~ formId %}
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function singleInvoice(link)
|
||||||
|
{
|
||||||
|
let formPlugin = kimai.getPlugin('form');
|
||||||
|
let data = formPlugin.getFormData(document.getElementById('{{ formId }}'));
|
||||||
|
let uri = formPlugin.convertFormDataToQueryString(data);
|
||||||
|
let baseUrl = link.getAttribute('data-href');
|
||||||
|
link.href = baseUrl + '?' + uri;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAllInvoices(link)
|
||||||
|
{
|
||||||
|
let formPlugin = kimai.getPlugin('form');
|
||||||
|
let data = formPlugin.getFormData(document.getElementById('{{ formId }}'));
|
||||||
|
let uri = formPlugin.convertFormDataToQueryString(data);
|
||||||
|
link.href = '{{ path('invoice') }}?createInvoice=true&' + uri;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('kimai.initialized', function() {
|
document.addEventListener('kimai.initialized', function() {
|
||||||
KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true);
|
KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
{% import "invoice/macros.html.twig" as macros %}
|
{% import "invoice/macros.html.twig" as macros %}
|
||||||
|
|
||||||
{% set columns = {
|
{% set columns = {
|
||||||
'date': {'class': 'alwaysVisible text-nowrap', 'orderBy': false},
|
'date': {'class': 'alwaysVisible w-min', 'orderBy': false},
|
||||||
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
|
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
|
||||||
'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
|
'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
|
||||||
'invoice_number': {'class': 'hidden-xs hidden-sm text-nowrap', 'title': 'invoice.number'|trans, 'orderBy': false},
|
'invoice_number': {'class': 'hidden-xs hidden-sm text-center w-min', 'title': 'invoice.number'|trans, 'orderBy': false},
|
||||||
'due_date': {'class': 'hidden-xs text-nowrap', 'title': 'invoice.due_days'|trans, 'orderBy': false},
|
'due_date': {'class': 'hidden-xs text-center w-min', 'title': 'invoice.due_days'|trans, 'orderBy': false},
|
||||||
'status': {'class': 'text-center alwaysVisible text-nowrap', 'orderBy': false},
|
'status': {'class': 'text-center alwaysVisible w-min', 'orderBy': false},
|
||||||
'tax': {'class': 'hidden-xs text-center text-nowrap hidden', 'title': 'invoice.tax'|trans, 'orderBy': false},
|
'tax': {'class': 'hidden-xs text-center w-min hidden', 'title': 'invoice.tax'|trans, 'orderBy': false},
|
||||||
'total_rate': {'class': 'hidden-xs text-center text-nowrap', 'orderBy': false},
|
'total_rate': {'class': 'hidden-xs text-right w-min', 'orderBy': false},
|
||||||
'actions': {'class': 'actions alwaysVisible', 'orderBy': false},
|
'actions': {'class': 'actions alwaysVisible', 'orderBy': false},
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,10 @@
|
|||||||
{% set translationPrefix = options.translationPrefix ?? 'label.' %}
|
{% set translationPrefix = options.translationPrefix ?? 'label.' %}
|
||||||
{% set boxClass = options.boxClass ?? 'box box-' ~ admin_lte_context.widget.type ~ ' data_table' %}
|
{% set boxClass = options.boxClass ?? 'box box-' ~ admin_lte_context.widget.type ~ ' data_table' %}
|
||||||
|
|
||||||
{% import _self as macro %}
|
<div class="{{ boxClass }} datatable_{{ tableName }}">
|
||||||
<div class="{{ boxClass }}" id="datatable_{{ tableName }}">
|
|
||||||
<div class="box-body no-padding">
|
<div class="box-body no-padding">
|
||||||
<div class="dataTables_wrapper form-inline dt-bootstrap">
|
<div class="dataTables_wrapper form-inline dt-bootstrap">
|
||||||
<table class="table {% if striped %}table-striped {% endif %}{% if bordered %}table-bordered {% endif %}table-hover dataTable" role="grid" data-reload-event="{{ reloadEvent }}" id="dt_{{ tableName }}">
|
<table class="table {% if striped %}table-striped {% endif %}{% if bordered %}table-bordered {% endif %}table-hover dataTable" role="grid" data-reload-event="{{ reloadEvent }}">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{%- for title, headerOptions in columns -%}
|
{%- for title, headerOptions in columns -%}
|
||||||
@@ -71,7 +70,7 @@
|
|||||||
{% set headerOptions = headerOptions|merge({'orderBy': title}) %}
|
{% set headerOptions = headerOptions|merge({'orderBy': title}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set headerClass = macro.data_table_column_class(tableName, columns, title) %}
|
{% set headerClass = _self.data_table_column_class(tableName, columns, title) %}
|
||||||
{% if title != 'actions' and not headerOptions.orderBy is same as(false) %}
|
{% if title != 'actions' and not headerOptions.orderBy is same as(false) %}
|
||||||
{% if orderBy == headerOptions.orderBy %}
|
{% if orderBy == headerOptions.orderBy %}
|
||||||
{% set headerClass = headerClass ~ ' sortable sorting_' ~ (order) %}
|
{% set headerClass = headerClass ~ ' sortable sorting_' ~ (order) %}
|
||||||
|
|||||||
@@ -11,39 +11,25 @@
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
{% set orderHasError = form.orderBy.vars.errors|length > 0 or form.order.vars.errors|length > 0 %}
|
||||||
{% set orderBy = form_widget(form.orderBy) %}
|
{% set orderBy = form_widget(form.orderBy) %}
|
||||||
{% set order = form_widget(form.order) %}
|
{% set order = form_widget(form.order) %}
|
||||||
{{ form(form) }}
|
{{ form(form) }}
|
||||||
<div class="form-group">
|
<div class="form-group{% if orderHasError %} has-error{% endif %}">
|
||||||
{{ form_label(form.orderBy) }}
|
{{ form_label(form.orderBy) }}
|
||||||
<div class="col-sm-5 col-xs-5">
|
<div class="col-sm-5 col-xs-5">
|
||||||
{{ orderBy|raw }}
|
{{ orderBy|raw }}
|
||||||
|
{{ form_errors(form.orderBy) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4 col-xs-3">
|
<div class="col-sm-4 col-xs-3">
|
||||||
{{ order|raw }}
|
{{ order|raw }}
|
||||||
|
{{ form_errors(form.order) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="btn-toolbar pull-left" role="toolbar">
|
<div class="btn-toolbar pull-left" role="toolbar">
|
||||||
<div class="btn-group">
|
{{ _self.searchButton(form) }}
|
||||||
<button type="submit" name="performSearch" value="performSearch" class="btn btn-primary pull-left" data-type="submit">{{ 'search'|trans }}</button>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
{% if form.vars.data.bookmark %}
|
|
||||||
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-default" title="{{ 'label.set_as_default'|trans }}"><i class="{{ 'bookmarked'|icon }}"></i></button>
|
|
||||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<a href="?removeDefaultQuery={{ form.vars.data.bookmark.name }}" id="removeDefaultQuery">{{ 'action.delete'|trans }}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-default" title="{{ 'label.set_as_default'|trans }}"><i class="{{ 'bookmark'|icon }}"></i></button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
|
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,3 +38,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro searchButton(form) %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="submit" name="performSearch" value="performSearch" class="btn btn-primary pull-left" data-type="submit">{{ 'search'|trans }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
{% if form.vars.data.bookmark %}
|
||||||
|
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-default" title="{{ 'label.set_as_default'|trans }}"><i class="{{ 'bookmarked'|icon }}"></i></button>
|
||||||
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a href="?removeDefaultQuery={{ form.vars.data.bookmark.name }}" id="removeDefaultQuery">{{ 'action.delete'|trans }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-default" title="{{ 'label.set_as_default'|trans }}"><i class="{{ 'bookmark'|icon }}"></i></button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ use App\Invoice\ServiceInvoice;
|
|||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
use App\Repository\InvoiceTemplateRepository;
|
use App\Repository\InvoiceTemplateRepository;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\TimesheetRepository;
|
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use App\Tests\DataFixtures\CustomerFixtures;
|
use App\Tests\DataFixtures\CustomerFixtures;
|
||||||
use App\Tests\DataFixtures\InvoiceTemplateFixtures;
|
use App\Tests\DataFixtures\InvoiceTemplateFixtures;
|
||||||
@@ -69,7 +68,6 @@ class InvoiceCreateCommandTest extends KernelTestCase
|
|||||||
|
|
||||||
$this->application->add(new InvoiceCreateCommand(
|
$this->application->add(new InvoiceCreateCommand(
|
||||||
$container->get(ServiceInvoice::class),
|
$container->get(ServiceInvoice::class),
|
||||||
$container->get(TimesheetRepository::class),
|
|
||||||
$container->get(CustomerRepository::class),
|
$container->get(CustomerRepository::class),
|
||||||
$container->get(ProjectRepository::class),
|
$container->get(ProjectRepository::class),
|
||||||
$container->get(InvoiceTemplateRepository::class),
|
$container->get(InvoiceTemplateRepository::class),
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ abstract class ControllerBaseTest extends WebTestCase
|
|||||||
*/
|
*/
|
||||||
protected function assertDataTableRowCount(HttpKernelBrowser $client, string $id, int $count)
|
protected function assertDataTableRowCount(HttpKernelBrowser $client, string $id, int $count)
|
||||||
{
|
{
|
||||||
$node = $client->getCrawler()->filter('section.content div#' . $id . ' table.table-striped tbody tr:not(.summary)');
|
$node = $client->getCrawler()->filter('section.content div.' . $id . ' table.table-striped tbody tr:not(.summary)');
|
||||||
self::assertEquals($count, $node->count());
|
self::assertEquals($count, $node->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +346,7 @@ abstract class ControllerBaseTest extends WebTestCase
|
|||||||
* @param HttpKernelBrowser $client
|
* @param HttpKernelBrowser $client
|
||||||
* @param string $url
|
* @param string $url
|
||||||
*/
|
*/
|
||||||
protected function assertIsRedirect(HttpKernelBrowser $client, $url = null)
|
protected function assertIsRedirect(HttpKernelBrowser $client, $url = null, $endsWith = true)
|
||||||
{
|
{
|
||||||
self::assertTrue($client->getResponse()->isRedirect(), 'Response is not a redirect');
|
self::assertTrue($client->getResponse()->isRedirect(), 'Response is not a redirect');
|
||||||
if (null === $url) {
|
if (null === $url) {
|
||||||
@@ -354,7 +354,19 @@ abstract class ControllerBaseTest extends WebTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
self::assertTrue($client->getResponse()->headers->has('Location'), 'Could not find "Location" header');
|
self::assertTrue($client->getResponse()->headers->has('Location'), 'Could not find "Location" header');
|
||||||
self::assertStringEndsWith($url, $client->getResponse()->headers->get('Location'), 'Redirect URL does not match');
|
if ($endsWith) {
|
||||||
|
self::assertStringEndsWith(
|
||||||
|
$url,
|
||||||
|
$client->getResponse()->headers->get('Location'),
|
||||||
|
'Redirect URL does not match'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self::assertStringContainsString(
|
||||||
|
$url,
|
||||||
|
$client->getResponse()->headers->get('Location'),
|
||||||
|
'Redirect URL does not match'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function assertExcelExportResponse(HttpKernelBrowser $client, string $prefix)
|
protected function assertExcelExportResponse(HttpKernelBrowser $client, string $prefix)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
$templates = $this->importFixture($fixture);
|
$templates = $this->importFixture($fixture);
|
||||||
$id = $templates[0]->getId();
|
$id = $templates[0]->getId();
|
||||||
|
|
||||||
$this->request($client, '/invoice/?customer=1&template=' . $id . '&preview=');
|
$this->request($client, '/invoice/?customers[]=1&template=' . $id);
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
$this->assertHasNoEntriesWithFilter($client);
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
@@ -168,12 +168,12 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
||||||
$node = $form->getFormNode();
|
$node = $form->getFormNode();
|
||||||
$node->setAttribute('action', $this->createUrl('/invoice/?preview='));
|
$node->setAttribute('action', $this->createUrl('/invoice/'));
|
||||||
$node->setAttribute('method', 'GET');
|
$node->setAttribute('method', 'GET');
|
||||||
$client->submit($form, [
|
$client->submit($form, [
|
||||||
'template' => $template->getId(),
|
'template' => $template->getId(),
|
||||||
'daterange' => $dateRange,
|
'daterange' => $dateRange,
|
||||||
'customer' => 1,
|
'customers' => [1],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
@@ -181,25 +181,20 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
// no warning should be displayed
|
// no warning should be displayed
|
||||||
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
||||||
$this->assertEquals(0, $node->count());
|
$this->assertEquals(0, $node->count());
|
||||||
// but the datatable with all timesheets + 1 row for the total
|
// but the datatable with all timesheets
|
||||||
$this->assertDataTableRowCount($client, 'datatable_invoice', 21);
|
$this->assertDataTableRowCount($client, 'datatable_invoice', 20);
|
||||||
|
|
||||||
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
$urlParams = [
|
||||||
$node = $form->getFormNode();
|
|
||||||
$node->setAttribute('action', $this->createUrl('/invoice/?create='));
|
|
||||||
$node->setAttribute('method', 'GET');
|
|
||||||
$client->submit($form, [
|
|
||||||
'template' => $template->getId(),
|
|
||||||
'daterange' => $dateRange,
|
'daterange' => $dateRange,
|
||||||
'customer' => 1,
|
'projects[]' => 1,
|
||||||
'projects' => [1],
|
|
||||||
'markAsExported' => 1,
|
'markAsExported' => 1,
|
||||||
]);
|
];
|
||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$action = '/invoice/save-invoice/1/' . $template->getId() . '?' . http_build_query($urlParams);
|
||||||
$node = $client->getCrawler()->filter('body');
|
$this->request($client, $action);
|
||||||
$this->assertEquals(1, $node->count());
|
$this->assertIsRedirect($client, '/invoice/show?id=', false);
|
||||||
$this->assertEquals('invoice_print', $node->getIterator()[0]->getAttribute('class'));
|
$client->followRedirect();
|
||||||
|
$this->assertDataTableRowCount($client, 'datatable_invoices', 1);
|
||||||
|
|
||||||
$em = $this->getEntityManager();
|
$em = $this->getEntityManager();
|
||||||
$em->clear();
|
$em->clear();
|
||||||
@@ -234,35 +229,13 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$dateRange = $begin->format('Y-m-d') . DateRangeType::DATE_SPACER . $end->format('Y-m-d');
|
$dateRange = $begin->format('Y-m-d') . DateRangeType::DATE_SPACER . $end->format('Y-m-d');
|
||||||
|
|
||||||
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
$params = [
|
||||||
$node = $form->getFormNode();
|
|
||||||
$node->setAttribute('action', $this->createUrl('/invoice/?preview='));
|
|
||||||
$node->setAttribute('method', 'GET');
|
|
||||||
$client->submit($form, [
|
|
||||||
'template' => $id,
|
|
||||||
'daterange' => $dateRange,
|
'daterange' => $dateRange,
|
||||||
'customer' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
|
||||||
|
|
||||||
// no warning should be displayed
|
|
||||||
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
|
||||||
$this->assertEquals(0, $node->count());
|
|
||||||
// but the datatable with all timesheets + 1 row for the total
|
|
||||||
$this->assertDataTableRowCount($client, 'datatable_invoice', 21);
|
|
||||||
|
|
||||||
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
|
||||||
$node = $form->getFormNode();
|
|
||||||
$node->setAttribute('action', $this->createUrl('/invoice/?print='));
|
|
||||||
$node->setAttribute('method', 'GET');
|
|
||||||
$client->submit($form, [
|
|
||||||
'template' => $id,
|
|
||||||
'daterange' => $dateRange,
|
|
||||||
'customer' => 1,
|
|
||||||
'projects' => [1],
|
'projects' => [1],
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
$action = '/invoice/preview/1/' . $id . '?' . http_build_query($params);
|
||||||
|
$this->request($client, $action);
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
$node = $client->getCrawler()->filter('body');
|
$node = $client->getCrawler()->filter('body');
|
||||||
$this->assertEquals(1, $node->count());
|
$this->assertEquals(1, $node->count());
|
||||||
@@ -299,7 +272,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
$client->submit($form, [
|
$client->submit($form, [
|
||||||
'template' => $template->getId(),
|
'template' => $template->getId(),
|
||||||
'daterange' => $dateRange,
|
'daterange' => $dateRange,
|
||||||
'customer' => 1,
|
'customers' => [1],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
@@ -307,17 +280,17 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
// no warning should be displayed
|
// no warning should be displayed
|
||||||
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
||||||
$this->assertEquals(0, $node->count());
|
$this->assertEquals(0, $node->count());
|
||||||
// but the datatable with all timesheets + 1 row for the total
|
// but the datatable with all timesheets
|
||||||
$this->assertDataTableRowCount($client, 'datatable_invoice', 21);
|
$this->assertDataTableRowCount($client, 'datatable_invoice', 20);
|
||||||
|
|
||||||
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
||||||
$node = $form->getFormNode();
|
$node = $form->getFormNode();
|
||||||
$node->setAttribute('action', $this->createUrl('/invoice/?create='));
|
$node->setAttribute('action', $this->createUrl('/invoice/?createInvoice=true'));
|
||||||
$node->setAttribute('method', 'GET');
|
$node->setAttribute('method', 'GET');
|
||||||
$client->submit($form, [
|
$client->submit($form, [
|
||||||
'template' => $template->getId(),
|
'template' => $template->getId(),
|
||||||
'daterange' => $dateRange,
|
'daterange' => $dateRange,
|
||||||
'customer' => 1,
|
'customers' => [1],
|
||||||
'projects' => [1],
|
'projects' => [1],
|
||||||
'markAsExported' => 1,
|
'markAsExported' => 1,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class PermissionControllerTest extends ControllerBaseTest
|
|||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||||
$this->assertAccessIsGranted($client, '/admin/permissions');
|
$this->assertAccessIsGranted($client, '/admin/permissions');
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasDataTable($client);
|
||||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 119);
|
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 118);
|
||||||
$this->assertPageActions($client, [
|
$this->assertPageActions($client, [
|
||||||
//'back' => $this->createUrl('/admin/user/'),
|
//'back' => $this->createUrl('/admin/user/'),
|
||||||
'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),
|
'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class ProjectViewControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$this->assertAccessIsGranted($client, '/reporting/project_view');
|
$this->assertAccessIsGranted($client, '/reporting/project_view');
|
||||||
self::assertStringContainsString('<div class="box-body project-view-reporting-box', $client->getResponse()->getContent());
|
self::assertStringContainsString('<div class="box-body project-view-reporting-box', $client->getResponse()->getContent());
|
||||||
$rows = $client->getCrawler()->filterXPath("//table[@id='dt_project_view_reporting']/tbody/tr");
|
$rows = $client->getCrawler()->filterXPath("//table[contains(@class, 'dataTable')]/tbody/tr[not(@class='summary')]");
|
||||||
self::assertGreaterThan(0, $rows->count());
|
self::assertGreaterThan(0, $rows->count());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$this->assertDataTableRowCount($client, 'datatable_timesheet', 7);
|
$this->assertDataTableRowCount($client, 'datatable_timesheet', 7);
|
||||||
|
|
||||||
// make sure the recording css class exist on tr for targeting running record rows
|
// make sure the recording css class exist on tr for targeting running record rows
|
||||||
$node = $client->getCrawler()->filter('section.content div#datatable_timesheet table.table-striped tbody tr.recording');
|
$node = $client->getCrawler()->filter('section.content div.datatable_timesheet table.table-striped tbody tr.recording');
|
||||||
self::assertEquals(2, $node->count());
|
self::assertEquals(2, $node->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
$this->assertDataTableRowCount($client, 'datatable_timesheet_admin', 13);
|
$this->assertDataTableRowCount($client, 'datatable_timesheet_admin', 13);
|
||||||
|
|
||||||
// make sure the recording css class exist on tr for targeting running record rows
|
// make sure the recording css class exist on tr for targeting running record rows
|
||||||
$node = $client->getCrawler()->filter('section.content div#datatable_timesheet_admin table.table-striped tbody tr.recording');
|
$node = $client->getCrawler()->filter('section.content div.datatable_timesheet_admin table.table-striped tbody tr.recording');
|
||||||
self::assertEquals(3, $node->count());
|
self::assertEquals(3, $node->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,14 @@
|
|||||||
namespace App\Tests\Invoice;
|
namespace App\Tests\Invoice;
|
||||||
|
|
||||||
use App\Configuration\LanguageFormattings;
|
use App\Configuration\LanguageFormattings;
|
||||||
|
use App\Entity\Customer;
|
||||||
use App\Entity\Invoice;
|
use App\Entity\Invoice;
|
||||||
use App\Entity\InvoiceDocument;
|
use App\Entity\InvoiceDocument;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
use App\Invoice\Calculator\DefaultCalculator;
|
use App\Invoice\Calculator\DefaultCalculator;
|
||||||
|
use App\Invoice\InvoiceItemRepositoryInterface;
|
||||||
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
||||||
use App\Invoice\Renderer\TwigRenderer;
|
use App\Invoice\Renderer\TwigRenderer;
|
||||||
use App\Invoice\ServiceInvoice;
|
use App\Invoice\ServiceInvoice;
|
||||||
@@ -93,11 +97,8 @@ class ServiceInvoiceTest extends TestCase
|
|||||||
|
|
||||||
$sut->addCalculator(new DefaultCalculator());
|
$sut->addCalculator(new DefaultCalculator());
|
||||||
$sut->addNumberGenerator($this->getNumberGeneratorSut());
|
$sut->addNumberGenerator($this->getNumberGeneratorSut());
|
||||||
$sut->addRenderer(
|
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
|
||||||
new TwigRenderer(
|
$sut->addRenderer(new TwigRenderer($twig));
|
||||||
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals(1, \count($sut->getCalculator()));
|
$this->assertEquals(1, \count($sut->getCalculator()));
|
||||||
$this->assertInstanceOf(DefaultCalculator::class, $sut->getCalculatorByName('default'));
|
$this->assertInstanceOf(DefaultCalculator::class, $sut->getCalculatorByName('default'));
|
||||||
@@ -113,8 +114,11 @@ class ServiceInvoiceTest extends TestCase
|
|||||||
$this->expectException(\Exception::class);
|
$this->expectException(\Exception::class);
|
||||||
$this->expectExceptionMessage('Cannot create invoice model without template');
|
$this->expectExceptionMessage('Cannot create invoice model without template');
|
||||||
|
|
||||||
|
$query = new InvoiceQuery();
|
||||||
|
$query->setCustomers([new Customer()]);
|
||||||
|
|
||||||
$sut = $this->getSut([]);
|
$sut = $this->getSut([]);
|
||||||
$sut->createModel(new InvoiceQuery());
|
$sut->createModel($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,10 +128,10 @@ class ServiceInvoiceTest extends TestCase
|
|||||||
{
|
{
|
||||||
$template = new InvoiceTemplate();
|
$template = new InvoiceTemplate();
|
||||||
$template->setNumberGenerator('date');
|
$template->setNumberGenerator('date');
|
||||||
|
|
||||||
self::assertNull($template->getLanguage());
|
self::assertNull($template->getLanguage());
|
||||||
|
|
||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
|
$query->setCustomers([new Customer()]);
|
||||||
$query->setTemplate($template);
|
$query->setTemplate($template);
|
||||||
|
|
||||||
$sut = $this->getSut([]);
|
$sut = $this->getSut([]);
|
||||||
@@ -139,6 +143,30 @@ class ServiceInvoiceTest extends TestCase
|
|||||||
self::assertEquals('en', $model->getTemplate()->getLanguage());
|
self::assertEquals('en', $model->getTemplate()->getLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
public function testFindInvoiceItemsWithoutCustomer()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut([]);
|
||||||
|
|
||||||
|
$query = new InvoiceQuery();
|
||||||
|
|
||||||
|
$items = $sut->findInvoiceItems($query);
|
||||||
|
self::assertEquals([], $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindInvoiceItemsWithCustomer()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut([]);
|
||||||
|
|
||||||
|
$query = new InvoiceQuery();
|
||||||
|
$query->setCustomers([new Customer(), new Customer()]);
|
||||||
|
|
||||||
|
$items = $sut->findInvoiceItems($query);
|
||||||
|
self::assertEquals([], $items);
|
||||||
|
}
|
||||||
|
|
||||||
public function testCreateModelUsesTemplateLanguage()
|
public function testCreateModelUsesTemplateLanguage()
|
||||||
{
|
{
|
||||||
$template = new InvoiceTemplate();
|
$template = new InvoiceTemplate();
|
||||||
@@ -148,6 +176,7 @@ class ServiceInvoiceTest extends TestCase
|
|||||||
self::assertEquals('de', $template->getLanguage());
|
self::assertEquals('de', $template->getLanguage());
|
||||||
|
|
||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
|
$query->setCustomers([new Customer()]);
|
||||||
$query->setTemplate($template);
|
$query->setTemplate($template);
|
||||||
|
|
||||||
$sut = $this->getSut([]);
|
$sut = $this->getSut([]);
|
||||||
@@ -159,6 +188,74 @@ class ServiceInvoiceTest extends TestCase
|
|||||||
self::assertEquals('de', $model->getTemplate()->getLanguage());
|
self::assertEquals('de', $model->getTemplate()->getLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBeginAndEndDateFallback()
|
||||||
|
{
|
||||||
|
$timezone = new \DateTimeZone('Europe/Vienna');
|
||||||
|
$customer = new Customer();
|
||||||
|
$project = new Project();
|
||||||
|
$project->setCustomer($customer);
|
||||||
|
|
||||||
|
$timesheet1 = new Timesheet();
|
||||||
|
$timesheet1->setProject($project);
|
||||||
|
$timesheet1->setBegin(new \DateTime('2011-01-27 12:12:12', $timezone));
|
||||||
|
$timesheet1->setEnd(new \DateTime('2020-01-27 12:12:12', $timezone));
|
||||||
|
|
||||||
|
$timesheet2 = new Timesheet();
|
||||||
|
$timesheet2->setProject($project);
|
||||||
|
$timesheet2->setBegin(new \DateTime('2010-01-27 08:24:33', $timezone));
|
||||||
|
$timesheet2->setEnd(new \DateTime('2019-01-27 12:12:12', $timezone));
|
||||||
|
|
||||||
|
$timesheet3 = new Timesheet();
|
||||||
|
$timesheet3->setProject($project);
|
||||||
|
$timesheet3->setBegin(new \DateTime('2019-01-27 12:12:12', $timezone));
|
||||||
|
$timesheet3->setEnd(new \DateTime('2020-01-07 12:12:12', $timezone));
|
||||||
|
|
||||||
|
$timesheet4 = new Timesheet();
|
||||||
|
$timesheet4->setProject($project);
|
||||||
|
$timesheet4->setBegin(new \DateTime('2020-01-27 10:12:12', $timezone));
|
||||||
|
$timesheet4->setEnd(new \DateTime('2020-11-27 11:12:12', $timezone));
|
||||||
|
|
||||||
|
$timesheet5 = new Timesheet();
|
||||||
|
$timesheet5->setProject($project);
|
||||||
|
$timesheet5->setBegin(new \DateTime('2012-01-27 12:12:12', $timezone));
|
||||||
|
$timesheet5->setEnd(new \DateTime('2018-01-27 12:12:12', $timezone));
|
||||||
|
|
||||||
|
$repo = $this->createMock(InvoiceItemRepositoryInterface::class);
|
||||||
|
$repo->method('getInvoiceItemsForQuery')->willReturn([
|
||||||
|
$timesheet1,
|
||||||
|
$timesheet2,
|
||||||
|
$timesheet3,
|
||||||
|
$timesheet4,
|
||||||
|
$timesheet5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$template = new InvoiceTemplate();
|
||||||
|
$template->setNumberGenerator('date');
|
||||||
|
$template->setLanguage('de');
|
||||||
|
|
||||||
|
self::assertEquals('de', $template->getLanguage());
|
||||||
|
|
||||||
|
$query = new InvoiceQuery();
|
||||||
|
$query->setCustomers([new Customer(), $customer]);
|
||||||
|
$query->setTemplate($template);
|
||||||
|
self::assertNull($query->getBegin());
|
||||||
|
self::assertNull($query->getEnd());
|
||||||
|
|
||||||
|
$sut = $this->getSut([]);
|
||||||
|
$sut->addCalculator(new DefaultCalculator());
|
||||||
|
$sut->addNumberGenerator($this->getNumberGeneratorSut());
|
||||||
|
|
||||||
|
$sut->addInvoiceItemRepository($repo);
|
||||||
|
|
||||||
|
$sut->createModels($query);
|
||||||
|
|
||||||
|
self::assertNotNull($query->getBegin());
|
||||||
|
self::assertNotNull($query->getEnd());
|
||||||
|
|
||||||
|
self::assertEquals('2010-01-27T00:00:00+0100', $query->getBegin()->format(DATE_ISO8601));
|
||||||
|
self::assertEquals('2020-11-27T23:59:59+0100', $query->getEnd()->format(DATE_ISO8601));
|
||||||
|
}
|
||||||
|
|
||||||
private function getNumberGeneratorSut()
|
private function getNumberGeneratorSut()
|
||||||
{
|
{
|
||||||
$repository = $this->createMock(InvoiceRepository::class);
|
$repository = $this->createMock(InvoiceRepository::class);
|
||||||
|
|||||||
@@ -733,10 +733,6 @@
|
|||||||
<source>invoice.title</source>
|
<source>invoice.title</source>
|
||||||
<target>Faktura</target>
|
<target>Faktura</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.preview">
|
|
||||||
<source>invoice.preview</source>
|
|
||||||
<target>Toto je náhled dat, která se zobrazí ve vašem fakturačním dokladu.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="button.print">
|
<trans-unit id="button.print">
|
||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
<target>Tisk</target>
|
<target>Tisk</target>
|
||||||
|
|||||||
@@ -391,6 +391,10 @@
|
|||||||
<source>action.save</source>
|
<source>action.save</source>
|
||||||
<target>Speichern</target>
|
<target>Speichern</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="action.save_all">
|
||||||
|
<source>action.save_all</source>
|
||||||
|
<target>Alle Speichern</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="action.reset">
|
<trans-unit id="action.reset">
|
||||||
<source>action.reset</source>
|
<source>action.reset</source>
|
||||||
<target>Zurücksetzen</target>
|
<target>Zurücksetzen</target>
|
||||||
@@ -1041,6 +1045,10 @@
|
|||||||
<source>status.paid</source>
|
<source>status.paid</source>
|
||||||
<target>Bezahlt</target>
|
<target>Bezahlt</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="preview.skipped_rows">
|
||||||
|
<source>preview.skipped_rows</source>
|
||||||
|
<target>Überspringe Vorschau von %rows% weiteren Zeilen ...</target>
|
||||||
|
</trans-unit>
|
||||||
<!--
|
<!--
|
||||||
Export
|
Export
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -395,6 +395,10 @@
|
|||||||
<source>action.save</source>
|
<source>action.save</source>
|
||||||
<target>Save</target>
|
<target>Save</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="action.save_all">
|
||||||
|
<source>action.save_all</source>
|
||||||
|
<target>Save all</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="action.reset">
|
<trans-unit id="action.reset">
|
||||||
<source>action.reset</source>
|
<source>action.reset</source>
|
||||||
<target>Reset</target>
|
<target>Reset</target>
|
||||||
@@ -1058,7 +1062,10 @@
|
|||||||
<source>status.paid</source>
|
<source>status.paid</source>
|
||||||
<target>Paid</target>
|
<target>Paid</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="preview.skipped_rows">
|
||||||
|
<source>preview.skipped_rows</source>
|
||||||
|
<target>Skipped preview of %rows% more rows ...</target>
|
||||||
|
</trans-unit>
|
||||||
<!--
|
<!--
|
||||||
Export
|
Export
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -693,10 +693,6 @@
|
|||||||
<source>invoice.title</source>
|
<source>invoice.title</source>
|
||||||
<target state="translated">인보이스</target>
|
<target state="translated">인보이스</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.preview">
|
|
||||||
<source>invoice.preview</source>
|
|
||||||
<target state="translated">인보이스에 표시되는 정보의 미리보기</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="button.print">
|
<trans-unit id="button.print">
|
||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
<target state="translated">인쇄</target>
|
<target state="translated">인쇄</target>
|
||||||
|
|||||||
Reference in New Issue
Block a user