added freelancer invoice template (#254)
This commit is contained in:
18
assets/app.js
Normal file
18
assets/app.js
Normal file
@@ -0,0 +1,18 @@
|
||||
require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
|
||||
|
||||
require('./sass/app.scss');
|
||||
|
||||
require('fullcalendar');
|
||||
require('fullcalendar/dist/gcal.min');
|
||||
require('fullcalendar/dist/locale-all');
|
||||
require('fullcalendar/dist/fullcalendar.min.css');
|
||||
|
||||
// ------ for charts ------
|
||||
require('chart.js/dist/Chart.min');
|
||||
|
||||
// ------ Kimai itself ------
|
||||
require('./js/kimai.js');
|
||||
require('./js/datatable.js');
|
||||
require('./js/toolbar.js');
|
||||
require('./images/default_avatar.png');
|
||||
require('./images/signature.png');
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.invoice {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.invoice {
|
||||
margin: 10px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-small{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div.invoice-address {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
table.invoice-records {}
|
||||
table.invoice-meta th {
|
||||
padding-right: 40px;
|
||||
}
|
||||
table.invoice-sum th {
|
||||
width: 70%;
|
||||
}
|
||||
table.invoice-sum th, table.invoice-sum td {
|
||||
text-align: right;
|
||||
}
|
||||
BIN
assets/images/signature.png
Normal file
BIN
assets/images/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,19 +0,0 @@
|
||||
require('../../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
|
||||
|
||||
require('../css/app.scss');
|
||||
require('../css/datatable.scss');
|
||||
require('../css/calendar.scss');
|
||||
|
||||
require('fullcalendar');
|
||||
require('fullcalendar/dist/gcal.min');
|
||||
require('fullcalendar/dist/locale-all');
|
||||
require('fullcalendar/dist/fullcalendar.min.css');
|
||||
|
||||
// ------ for charts ------
|
||||
require('chart.js/dist/Chart.min');
|
||||
|
||||
// ------ Kimai itself ------
|
||||
require('./kimai.js');
|
||||
require('./datatable.js');
|
||||
require('./toolbar.js');
|
||||
require('../images/default_avatar.png');
|
||||
299
assets/sass/invoice.scss
Normal file
299
assets/sass/invoice.scss
Normal file
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.invoice {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.invoice {
|
||||
margin: 10px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-small{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div.invoice-address {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
table.invoice-records {}
|
||||
table.invoice-meta th {
|
||||
padding-right: 40px;
|
||||
}
|
||||
table.invoice-sum th {
|
||||
width: 70%;
|
||||
}
|
||||
table.invoice-sum th, table.invoice-sum td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ================================ FREELANCER INVOICE ================================ */
|
||||
|
||||
#freelancer-invoice {
|
||||
font: 16px/1 'Open Sans', sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
||||
h1 {
|
||||
font: bold 100% sans-serif;
|
||||
letter-spacing: 0.5em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font: bold 1.5em sans-serif;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* table */
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* header */
|
||||
|
||||
header {
|
||||
}
|
||||
|
||||
header:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
header address {
|
||||
float: left;
|
||||
font-style: normal;
|
||||
line-height: 1.25;
|
||||
margin: 0 1em 1em 0;
|
||||
font-weight: bold;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
article address {
|
||||
font-size: 1em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
article.address {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
article.footer {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/* article */
|
||||
|
||||
article, article address, table.meta, table.inventory {
|
||||
margin: 2em 0 2em;
|
||||
}
|
||||
|
||||
article:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
clip: rect(0 0 0 0);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
article p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* table meta & balance */
|
||||
|
||||
table.meta:after, table.balance:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* table meta */
|
||||
|
||||
table.meta {
|
||||
float: right;
|
||||
width: 50%;
|
||||
font-size: 90%;
|
||||
|
||||
th {
|
||||
text-align: right;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
/* table balance */
|
||||
|
||||
table.balance {
|
||||
float: right;
|
||||
th {
|
||||
text-align: right;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* table items */
|
||||
|
||||
table.inventory {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
margin-top: 2em;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
table.inventory th {
|
||||
font-weight: bold;
|
||||
border-width: 0 0 1px 0;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
table.inventory th,
|
||||
table.inventory td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(1),
|
||||
table.inventory th:nth-child(1) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(2),
|
||||
table.inventory th:nth-child(2) {
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(3),
|
||||
table.inventory th:nth-child(3) {
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(4),
|
||||
table.inventory th:nth-child(4) {
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
/* footer */
|
||||
|
||||
footer {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
footer table {
|
||||
border-top: 1px solid #000;
|
||||
font-size: 0.8em;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
footer th {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
footer table th:nth-child(1) {
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
footer table th:nth-child(3) {
|
||||
width: 28%;
|
||||
}
|
||||
|
||||
footer td {
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
/* aside */
|
||||
|
||||
aside {
|
||||
position: absolute;
|
||||
top: 10in;
|
||||
width: 7.5in;
|
||||
}
|
||||
|
||||
aside h1 {
|
||||
border: none;
|
||||
border-width: 0 0 1px;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
aside h1 {
|
||||
border-color: #999;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
aside p {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
aside div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
aside div p {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
aside p span {
|
||||
width: 130px;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
* {
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
body, .invoice, .wrapper, #freelancer-invoice {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#freelancer-invoice {
|
||||
margin: 1em 2em;
|
||||
|
||||
span:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.add, .cut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,9 @@ kimai:
|
||||
# All configs related to the invoice administration
|
||||
invoice:
|
||||
renderer:
|
||||
default: 'App\Controller\InvoiceController::invoiceAction'
|
||||
timesheet: 'App\Controller\InvoiceController::timesheetAction'
|
||||
default: 'App\Controller\InvoicePrintController::printInvoice'
|
||||
timesheet: 'App\Controller\InvoicePrintController::printTimesheet'
|
||||
freelancer: 'App\Controller\InvoicePrintController::printFreelancer'
|
||||
calculator:
|
||||
default: 'App\Invoice\DefaultCalculator'
|
||||
short: 'App\Invoice\ShortInvoiceCalculator'
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<env name="DATABASE_PREFIX" value="kimai2_" force="true"/>
|
||||
<!-- define your env variables for the test env here -->
|
||||
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak_vendors"/>
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
||||
|
||||
<!-- ###+ doctrine/doctrine-bundle ### -->
|
||||
<env name="DATABASE_URL" value="sqlite:///var/data/kimai_test.sqlite" force="true"/>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
public/build/images/signature.png
Normal file
BIN
public/build/images/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"build/app.js": "/build/app.js?a67489c4fc07f30e6f86",
|
||||
"build/app.css": "/build/app.css?7ddba25b9e0ab7dec9690f128f8691c0",
|
||||
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
|
||||
"build/app.js": "/build/app.js?ba9725f5267413a18701",
|
||||
"build/app.css": "/build/app.css?68655639f068cc8b6eadb4170698a2c5",
|
||||
"build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?d9e29124",
|
||||
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
|
||||
"build/images/fa-solid-900.svg": "/build/images/fa-solid-900.svg?666a82cb",
|
||||
@@ -13,8 +13,8 @@
|
||||
"build/fonts/fa-brands-400.woff": "/build/fonts/fa-brands-400.woff?e9f83339",
|
||||
"build/fonts/glyphicons-halflings-regular.woff2": "/build/fonts/glyphicons-halflings-regular.woff2?448c34a5",
|
||||
"build/fonts/fa-solid-900.eot": "/build/fonts/fa-solid-900.eot?0b93480e",
|
||||
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
|
||||
"build/images/blue.png": "/build/images/blue.png?96f8a905",
|
||||
"build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?d9e29124",
|
||||
"build/fonts/fa-regular-400.woff2": "/build/fonts/fa-regular-400.woff2?930c1264",
|
||||
"build/images/fa-regular-400.svg": "/build/images/fa-regular-400.svg?4e41caaf",
|
||||
"build/fonts/fa-regular-400.ttf": "/build/fonts/fa-regular-400.ttf?cdd8fa76",
|
||||
@@ -23,5 +23,6 @@
|
||||
"build/fonts/fa-brands-400.woff2": "/build/fonts/fa-brands-400.woff2?66f625f1",
|
||||
"build/fonts/glyphicons-halflings-regular.ttf": "/build/fonts/glyphicons-halflings-regular.ttf?e18bbf61",
|
||||
"build/images/fa-brands-400.svg": "/build/images/fa-brands-400.svg?5da7e33c",
|
||||
"build/images/default_avatar.png": "/build/images/default_avatar.png?fc197350"
|
||||
"build/images/default_avatar.png": "/build/images/default_avatar.png?fc197350",
|
||||
"build/images/signature.png": "/build/images/signature.png?5f58501e"
|
||||
}
|
||||
@@ -92,23 +92,55 @@ class InvoiceController extends AbstractController
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
$query->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
|
||||
if (null !== $query->getCustomer()) {
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
|
||||
$queryBuilder = $this->timesheetRepository->findByQuery($query);
|
||||
$entries = $queryBuilder->getQuery()->getResult();
|
||||
}
|
||||
$entries = $this->getEntries($query);
|
||||
}
|
||||
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
|
||||
$action = null;
|
||||
if ($query->getTemplate() !== null) {
|
||||
$action = $this->service->getRendererActionByName($query->getTemplate()->getRenderer());
|
||||
}
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'action' => $action,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getEntries(InvoiceQuery $query)
|
||||
{
|
||||
if (null === $query->getCustomer()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
|
||||
$queryBuilder = $this->timesheetRepository->findByQuery($query);
|
||||
|
||||
return $queryBuilder->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param array $entries
|
||||
* @return InvoiceModel
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function prepareModel(InvoiceQuery $query, array $entries)
|
||||
{
|
||||
$model = new InvoiceModel();
|
||||
$model->setQuery($query);
|
||||
$model->setEntries($entries);
|
||||
@@ -129,14 +161,9 @@ class InvoiceController extends AbstractController
|
||||
$model->setTemplate($query->getTemplate());
|
||||
$model->setCalculator($calculator);
|
||||
$model->setNumberGenerator($generator);
|
||||
$action = $this->service->getRendererActionByName($query->getTemplate()->getRenderer());
|
||||
}
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'action' => $action,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,6 +256,7 @@ class InvoiceController extends AbstractController
|
||||
try {
|
||||
$this->invoiceRepository->saveTemplate($template);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
@@ -241,28 +269,6 @@ class InvoiceController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function invoiceAction(InvoiceModel $model)
|
||||
{
|
||||
return $this->render('invoice/renderer/print.html.twig', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function timesheetAction(InvoiceModel $model)
|
||||
{
|
||||
return $this->render('invoice/renderer/timesheet.html.twig', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
|
||||
54
src/Controller/InvoicePrintController.php
Normal file
54
src/Controller/InvoicePrintController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Model\InvoiceModel;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
|
||||
/**
|
||||
* Controller used to print invoices.
|
||||
*
|
||||
* @Security("is_granted('ROLE_TEAMLEAD')")
|
||||
*/
|
||||
class InvoicePrintController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function printInvoice(InvoiceModel $model)
|
||||
{
|
||||
return $this->render('invoice/renderer/invoice.html.twig', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function printTimesheet(InvoiceModel $model)
|
||||
{
|
||||
return $this->render('invoice/renderer/timesheet.html.twig', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function printFreelancer(InvoiceModel $model)
|
||||
{
|
||||
return $this->render('invoice/renderer/freelancer.html.twig', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -28,13 +28,15 @@ class InvoiceFixtures extends Fixture
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$this->loadInvoiceTemplates($manager);
|
||||
$this->loadInvoiceTemplate($manager);
|
||||
$this->loadFreelancerTemplate($manager);
|
||||
$this->loadTimesheetTemplate($manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
private function loadInvoiceTemplates(ObjectManager $manager)
|
||||
private function loadInvoiceTemplate(ObjectManager $manager)
|
||||
{
|
||||
$faker = Factory::create();
|
||||
|
||||
@@ -44,7 +46,10 @@ class InvoiceFixtures extends Fixture
|
||||
->setTitle('Your company name')
|
||||
->setCompany($faker->company)
|
||||
->setVat(19)
|
||||
->setDueDays(14)
|
||||
->setDueDays(30)
|
||||
->setRenderer('default')
|
||||
->setCalculator('default')
|
||||
->setNumberGenerator('default')
|
||||
->setPaymentTerms(
|
||||
'I would like to thank you for your confidence and will gladly be there for you in the future.' .
|
||||
PHP_EOL .
|
||||
@@ -62,4 +67,68 @@ class InvoiceFixtures extends Fixture
|
||||
$manager->persist($template);
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
private function loadFreelancerTemplate(ObjectManager $manager)
|
||||
{
|
||||
$faker = Factory::create();
|
||||
|
||||
$template = new InvoiceTemplate();
|
||||
$template
|
||||
->setName('Freelancer')
|
||||
->setTitle('Rechnung')
|
||||
->setCompany($faker->company)
|
||||
->setVat(19)
|
||||
->setDueDays(14)
|
||||
->setRenderer('freelancer')
|
||||
->setCalculator('short')
|
||||
->setNumberGenerator('default')
|
||||
->setPaymentTerms(
|
||||
'Bitte überweisen Sie den Gesamtbetrag innerhalb von 14 Tagen nach Erhalt der Rechnung auf das unten genannte Konto. Verwenden Sie bitte als Betreff Ihrer Überweisung die Rechnungsnummer.' .
|
||||
PHP_EOL .
|
||||
PHP_EOL .
|
||||
'Ich bedanke mich für das entgegengebrachte Vertrauen. Gerne bin ich auch künftig für Sie da.' .
|
||||
PHP_EOL .
|
||||
PHP_EOL .
|
||||
'Mit freundlichen Grüßen,' .
|
||||
PHP_EOL .
|
||||
'Max Müller'
|
||||
)
|
||||
->setAddress(
|
||||
$faker->name . ' - ' . $faker->streetAddress . '-' . $faker->postcode . ' ' . $faker->city
|
||||
)
|
||||
;
|
||||
|
||||
$manager->persist($template);
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
private function loadTimesheetTemplate(ObjectManager $manager)
|
||||
{
|
||||
$faker = Factory::create();
|
||||
|
||||
$template = new InvoiceTemplate();
|
||||
$template
|
||||
->setName('Timesheet')
|
||||
->setTitle('Stundenzettel')
|
||||
->setCompany($faker->company)
|
||||
->setVat(19)
|
||||
->setDueDays(14)
|
||||
->setRenderer('timesheet')
|
||||
->setCalculator('default')
|
||||
->setNumberGenerator('default')
|
||||
->setPaymentTerms('')
|
||||
->setAddress(
|
||||
$faker->name . ' - ' . $faker->streetAddress . '-' . $faker->postcode . ' ' . $faker->city
|
||||
)
|
||||
;
|
||||
|
||||
$manager->persist($template);
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,4 +58,18 @@ interface CalculatorInterface
|
||||
* @return string
|
||||
*/
|
||||
public function getCurrency(): string;
|
||||
|
||||
/**
|
||||
* Returns the percentage for the value-added tax (VAT) calculation.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getVat(): ?float;
|
||||
|
||||
/**
|
||||
* Returns the total amount of worked time in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTimeWorked(): int;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class DefaultCalculator implements CalculatorInterface
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getVat()
|
||||
public function getVat(): ?float
|
||||
{
|
||||
return $this->model->getTemplate()->getVat();
|
||||
}
|
||||
@@ -97,4 +97,19 @@ class DefaultCalculator implements CalculatorInterface
|
||||
{
|
||||
return $this->model->getCustomer()->getCurrency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total amount of worked time in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTimeWorked(): int
|
||||
{
|
||||
$time = 0;
|
||||
foreach ($this->model->getEntries() as $entry) {
|
||||
$time += $entry->getDuration();
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,16 @@ class ShortInvoiceCalculator extends DefaultCalculator
|
||||
foreach ($this->model->getEntries() as $entry) {
|
||||
$timesheet->setRate($timesheet->getRate() + $entry->getRate());
|
||||
$timesheet->setDuration($timesheet->getDuration() + $entry->getDuration());
|
||||
$timesheet->setBegin($entry->getBegin());
|
||||
if (null === $timesheet->getActivity()) {
|
||||
$timesheet->setActivity($entry->getActivity());
|
||||
$timesheet->setEnd($entry->getEnd());
|
||||
}
|
||||
$timesheet->setBegin($entry->getBegin());
|
||||
}
|
||||
|
||||
if (null !== $this->model->getQuery()->getActivity()) {
|
||||
$timesheet->setDescription($this->model->getQuery()->getActivity()->getName());
|
||||
} elseif (null !== $this->model->getQuery()->getProject()) {
|
||||
$timesheet->setDescription($this->model->getQuery()->getProject()->getName());
|
||||
}
|
||||
|
||||
return [$timesheet];
|
||||
|
||||
@@ -34,7 +34,7 @@ class InvoiceModel
|
||||
/**
|
||||
* @var Timesheet[]
|
||||
*/
|
||||
protected $entries;
|
||||
protected $entries = [];
|
||||
|
||||
/**
|
||||
* @var InvoiceTemplate
|
||||
|
||||
@@ -37,6 +37,7 @@ class Markdown
|
||||
public function toHtml(string $text, bool $safe = true): string
|
||||
{
|
||||
$this->parser->setSafeMode($safe);
|
||||
|
||||
return $this->parser->text($text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
|
||||
{% if back %}
|
||||
{% if back|default(false) %}
|
||||
<a href="{{ back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{% if model.entries is empty or action is empty %}
|
||||
{{ widgets.callout('warning', 'invoice.select_filter') }}
|
||||
{% else %}
|
||||
{{ render(controller(action, {'model': model})) }}
|
||||
{{ render(controller(action, {'model': model, 'appRequest': app.request})) }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
147
templates/invoice/renderer/freelancer.html.twig
Normal file
147
templates/invoice/renderer/freelancer.html.twig
Normal file
@@ -0,0 +1,147 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{# You can overrwite these settings in your config/packages.local.yaml under the key "twig.globals.company" #}
|
||||
{% set company = company|default({
|
||||
name: 'Kimai Inc.',
|
||||
homepage: 'www.kimai.org',
|
||||
email: 'kimai@example.com',
|
||||
phone: '0123-4567890',
|
||||
tax_number: 'YourTaxNumber',
|
||||
signature: '/build/images/signature.png',
|
||||
address: 'Kimai Inc.
|
||||
Example road 42
|
||||
D – 12345 City
|
||||
',
|
||||
bank_account: '
|
||||
Kimai Inc.
|
||||
IBAN: DE00 0000 0000 0000 0000 00
|
||||
BIC: XXXXXXXX (a bank name)
|
||||
'
|
||||
}) %}
|
||||
|
||||
<div class="row" id="freelancer-invoice" style="page:invoiceFreelancer;">
|
||||
<div class="col-xs-12">
|
||||
<header>
|
||||
<address>
|
||||
<p contenteditable="true">{{ model.template.address }}</p>
|
||||
</address>
|
||||
</header>
|
||||
<article class="address">
|
||||
<h1>{{ 'invoice.to'|trans }}</h1>
|
||||
<address>
|
||||
<p>
|
||||
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
|
||||
{{ model.customer.address|nl2br }}
|
||||
</p>
|
||||
</address>
|
||||
<table class="meta">
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}:</th>
|
||||
<td contenteditable="true">{{ model.invoiceDate|date_short }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.service_date'|trans }}:</th>
|
||||
<td><span contenteditable="true">{{ model.query.end|month_name|trans }} {{ model.query.end|date('Y') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.number'|trans }}:</th>
|
||||
<td><span contenteditable="true">{{ model.numberGenerator.invoiceNumber }}</td>
|
||||
</tr>
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.order_number'|trans }}</th>
|
||||
<td contenteditable="true">
|
||||
{{ model.query.project.orderNumber }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</article>
|
||||
<article>
|
||||
<h2>{{ model.template.title|default('invoice_renderer.timesheet'|trans) }}</h2>
|
||||
<p contenteditable="true">
|
||||
{{ 'label.invoice_salutation'|trans|nl2br }}
|
||||
</p>
|
||||
<table class="inventory">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.description'|trans }}</th>
|
||||
<th>{{ 'invoice.unit_price'|trans }}</th>
|
||||
<th>{{ 'invoice.amount'|trans }}</th>
|
||||
<th>{{ 'invoice.total_rate'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in model.calculator.entries %}
|
||||
<tr>
|
||||
<td contenteditable="true">{{ entry.description }}</td>
|
||||
<td contenteditable="true">{{ app.user.getPreferenceValue('hourly_rate')|money(model.calculator.currency) }}</td>
|
||||
<td contenteditable="true">{{ entry.duration|duration }}</td>
|
||||
<td>{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="balance">
|
||||
<tr>
|
||||
<th>{{ 'invoice.subtotal'|trans }}</th>
|
||||
<td>{{ model.calculator.subtotal|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)</th>
|
||||
<td>{{ model.calculator.tax|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="total">{{ 'invoice.total'|trans }}</th>
|
||||
<td class="total">{{ model.calculator.total|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</article>
|
||||
{% if model.template.paymentTerms is not empty %}
|
||||
<article class="footer">
|
||||
<p contenteditable="true">
|
||||
{{ model.template.paymentTerms|trim|nl2br }}
|
||||
</p>
|
||||
{% if company.signature is defined %}
|
||||
<img src="{{ company.signature }}">
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endif %}
|
||||
<footer>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.address'|trans }}</th>
|
||||
<th>{{ 'label.invoice_bank_account'|trans }}</th>
|
||||
<th>{{ 'label.contact'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td contenteditable="true">
|
||||
{{ company.address|nl2br }}
|
||||
{{ 'label.invoice_tax_number'|trans }} {{ company.tax_number }}
|
||||
</td>
|
||||
<td contenteditable="true">
|
||||
{{ company.bank_account|nl2br }}
|
||||
</td>
|
||||
<td contenteditable="true">
|
||||
{{ company.phone }}
|
||||
<br/>
|
||||
{{ company.email }}
|
||||
<br/>
|
||||
{{ company.homepage }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-print">
|
||||
<div class="col-xs-12">
|
||||
<button type="button" class="btn btn-success pull-right" onclick="window.print();return false;">
|
||||
<i class="{{ 'print'|icon }}"></i> {{ 'button.print'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,21 +51,23 @@
|
||||
<div class="col-xs-12 table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}</th>
|
||||
<th>{{ 'label.activity'|trans }}</th>
|
||||
<th>{{ 'label.hours'|trans }}</th>
|
||||
<th>{{ 'label.rate'|trans }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}</th>
|
||||
<th>{{ 'label.activity'|trans }}</th>
|
||||
<th>{{ 'invoice.unit_price'|trans }}</th>
|
||||
<th>{{ 'label.hours'|trans }}</th>
|
||||
<th>{{ 'invoice.total_rate'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in model.calculator.entries %}
|
||||
<tr>
|
||||
<td>{{ entry.begin|date_short }}</td>
|
||||
<td>{{ entry.activity.name }} / {{ entry.activity.project.name }}</td>
|
||||
<td>{{ entry.duration|duration }}</td>
|
||||
<td>{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ entry.begin|date_short }}</td>
|
||||
<td>{{ entry.activity.name }} / {{ entry.activity.project.name }}</td>
|
||||
<td contenteditable="true">{{ app.user.getPreferenceValue('hourly_rate')|money(model.calculator.currency) }}</td>
|
||||
<td>{{ entry.duration|duration }}</td>
|
||||
<td>{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -109,7 +111,7 @@
|
||||
|
||||
<div class="row no-print">
|
||||
<div class="col-xs-12">
|
||||
<button type="button" class="btn btn-success pull-right" onclick="window.print();return false;" style="margin-right: 5px;">
|
||||
<button type="button" class="btn btn-success pull-right" onclick="window.print();return false;">
|
||||
<i class="{{ 'print'|icon }}"></i> {{ 'button.print'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -23,13 +23,13 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:50%">{{ 'label.date'|trans }}</th>
|
||||
<td contenteditable="true">{{ model.invoiceDate|month_name|trans }} {{ model.invoiceDate|date('Y') }}</td>
|
||||
<td contenteditable="true">{{ model.query.end|month_name|trans }} {{ model.query.end|date('Y') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.customer'|trans }}</th>
|
||||
<td contenteditable="true">
|
||||
{% if model.customer.number is not empty %}[{{ model.customer.number }}]{% endif %}
|
||||
{{ model.customer.name }} / {{ model.customer.contact }}
|
||||
{{ model.customer.name }}{% if model.customer.contact is not empty %} / {{ model.customer.contact }}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
@@ -63,6 +63,13 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ 'invoice.total_working_time'|trans }}</th>
|
||||
<th>{{ model.calculator.timeWorked|duration }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +102,7 @@
|
||||
|
||||
<div class="row no-print">
|
||||
<div class="col-xs-12">
|
||||
<button type="button" class="btn btn-success pull-right" onclick="window.print();return false;" style="margin-right: 5px;">
|
||||
<button type="button" class="btn btn-success pull-right" onclick="window.print();return false;">
|
||||
<i class="{{ 'print'|icon }}"></i> {{ 'button.print'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,59 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
$this->assertHasDataTable($client);
|
||||
}
|
||||
|
||||
public function testCreateTemplateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->request($client, '/invoice/template/create');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=invoice_template_form]')->form();
|
||||
$client->submit($form, [
|
||||
'invoice_template_form' => [
|
||||
'name' => 'Test',
|
||||
'title' => 'Test invoice template',
|
||||
'company' => 'Company name',
|
||||
'renderer' => 'default',
|
||||
'calculator' => 'default',
|
||||
'numberGenerator' => 'default',
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/invoice/template'));
|
||||
$client->followRedirect();
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$this->assertHasFlashSuccess($client);
|
||||
}
|
||||
|
||||
public function testEditTemplateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new InvoiceFixtures();
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/invoice/template/1/edit?page=1');
|
||||
$form = $client->getCrawler()->filter('form[name=invoice_template_form]')->form();
|
||||
$client->submit($form, [
|
||||
'invoice_template_form' => [
|
||||
'name' => 'Test 2!',
|
||||
'title' => 'Test invoice template',
|
||||
'company' => 'Company name',
|
||||
'renderer' => 'default',
|
||||
'calculator' => 'default',
|
||||
'numberGenerator' => 'default',
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/invoice/template'));
|
||||
$client->followRedirect();
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$this->assertHasFlashSuccess($client);
|
||||
}
|
||||
|
||||
public function testDeleteTemplateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Voter;
|
||||
namespace App\Tests\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\DashboardEvent;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Voter;
|
||||
namespace App\Tests\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\EventSubscriber\RegistrationSubscriber;
|
||||
|
||||
79
tests/Invoice/DefaultCalculatorTest.php
Normal file
79
tests/Invoice/DefaultCalculatorTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Invoice;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Invoice\DefaultCalculator;
|
||||
use App\Model\InvoiceModel;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\DefaultCalculator
|
||||
*/
|
||||
class DefaultCalculatorTest extends TestCase
|
||||
{
|
||||
public function testEmptyModel()
|
||||
{
|
||||
$customer = new Customer();
|
||||
$template = new InvoiceTemplate();
|
||||
|
||||
$model = new InvoiceModel();
|
||||
$model->setCustomer($customer);
|
||||
$model->setTemplate($template);
|
||||
|
||||
$sut = new DefaultCalculator();
|
||||
$sut->setModel($model);
|
||||
|
||||
$this->assertEquals(0, $sut->getTotal());
|
||||
$this->assertEquals(0, $sut->getVat());
|
||||
$this->assertEquals('EUR', $sut->getCurrency());
|
||||
$this->assertEquals(0, $sut->getSubtotal());
|
||||
$this->assertEquals(0, $sut->getTimeWorked());
|
||||
$this->assertEquals([], $sut->getEntries());
|
||||
}
|
||||
|
||||
public function testWithMultipleEntries()
|
||||
{
|
||||
$customer = new Customer();
|
||||
$template = new InvoiceTemplate();
|
||||
$template->setVat(19);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setDuration(3600);
|
||||
$timesheet->setRate(293.27);
|
||||
|
||||
$timesheet2 = new Timesheet();
|
||||
$timesheet2->setDuration(400);
|
||||
$timesheet2->setRate(84);
|
||||
|
||||
$timesheet3 = new Timesheet();
|
||||
$timesheet3->setDuration(1800);
|
||||
$timesheet3->setRate(111.11);
|
||||
|
||||
$entries = [$timesheet, $timesheet2, $timesheet3];
|
||||
|
||||
$model = new InvoiceModel();
|
||||
$model->setCustomer($customer);
|
||||
$model->setTemplate($template);
|
||||
$model->setEntries($entries);
|
||||
|
||||
$sut = new DefaultCalculator();
|
||||
$sut->setModel($model);
|
||||
|
||||
$this->assertEquals(581.17, $sut->getTotal());
|
||||
$this->assertEquals(19, $sut->getVat());
|
||||
$this->assertEquals('EUR', $sut->getCurrency());
|
||||
$this->assertEquals(488.38, $sut->getSubtotal());
|
||||
$this->assertEquals(5800, $sut->getTimeWorked());
|
||||
$this->assertEquals($entries, $sut->getEntries());
|
||||
}
|
||||
}
|
||||
124
tests/Invoice/ShortInvoiceCalculatorTest.php
Normal file
124
tests/Invoice/ShortInvoiceCalculatorTest.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Invoice;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Invoice\ShortInvoiceCalculator;
|
||||
use App\Model\InvoiceModel;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\ShortInvoiceCalculator
|
||||
*/
|
||||
class ShortInvoiceCalculatorTest extends TestCase
|
||||
{
|
||||
public function testEmptyModel()
|
||||
{
|
||||
$customer = new Customer();
|
||||
$template = new InvoiceTemplate();
|
||||
$query = new InvoiceQuery();
|
||||
|
||||
$model = new InvoiceModel();
|
||||
$model->setCustomer($customer);
|
||||
$model->setTemplate($template);
|
||||
$model->setQuery($query);
|
||||
|
||||
$sut = new ShortInvoiceCalculator();
|
||||
$sut->setModel($model);
|
||||
|
||||
$this->assertEquals(0, $sut->getTotal());
|
||||
$this->assertEquals(0, $sut->getVat());
|
||||
$this->assertEquals('EUR', $sut->getCurrency());
|
||||
$this->assertEquals(0, $sut->getSubtotal());
|
||||
$this->assertEquals(0, $sut->getTimeWorked());
|
||||
$this->assertEquals(1, count($sut->getEntries()));
|
||||
}
|
||||
|
||||
public function testWithMultipleEntries()
|
||||
{
|
||||
$customer = new Customer();
|
||||
$template = new InvoiceTemplate();
|
||||
$template->setVat(19);
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->setName('activity description');
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setDuration(3600);
|
||||
$timesheet->setRate(293.27);
|
||||
|
||||
$timesheet2 = new Timesheet();
|
||||
$timesheet2->setDuration(400);
|
||||
$timesheet2->setRate(84);
|
||||
|
||||
$timesheet3 = new Timesheet();
|
||||
$timesheet3->setDuration(1800);
|
||||
$timesheet3->setRate(111.11);
|
||||
|
||||
$entries = [$timesheet, $timesheet2, $timesheet3];
|
||||
|
||||
$query = new InvoiceQuery();
|
||||
$query->setActivity($activity);
|
||||
|
||||
$model = new InvoiceModel();
|
||||
$model->setCustomer($customer);
|
||||
$model->setTemplate($template);
|
||||
$model->setEntries($entries);
|
||||
$model->setQuery($query);
|
||||
|
||||
$sut = new ShortInvoiceCalculator();
|
||||
$sut->setModel($model);
|
||||
|
||||
$this->assertEquals(581.17, $sut->getTotal());
|
||||
$this->assertEquals(19, $sut->getVat());
|
||||
$this->assertEquals('EUR', $sut->getCurrency());
|
||||
$this->assertEquals(488.38, $sut->getSubtotal());
|
||||
$this->assertEquals(5800, $sut->getTimeWorked());
|
||||
$this->assertEquals(1, count($sut->getEntries()));
|
||||
|
||||
/** @var Timesheet $result */
|
||||
$result = $sut->getEntries()[0];
|
||||
$this->assertEquals('activity description', $result->getDescription());
|
||||
$this->assertEquals(488.38, $result->getRate());
|
||||
$this->assertEquals(5800, $result->getDuration());
|
||||
}
|
||||
|
||||
public function testDescriptionByProject()
|
||||
{
|
||||
$customer = new Customer();
|
||||
$template = new InvoiceTemplate();
|
||||
$template->setVat(19);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('project description');
|
||||
|
||||
$query = new InvoiceQuery();
|
||||
$query->setProject($project);
|
||||
|
||||
$model = new InvoiceModel();
|
||||
$model->setCustomer($customer);
|
||||
$model->setTemplate($template);
|
||||
$model->setEntries([]);
|
||||
$model->setQuery($query);
|
||||
|
||||
$sut = new ShortInvoiceCalculator();
|
||||
$sut->setModel($model);
|
||||
$this->assertEquals(1, count($sut->getEntries()));
|
||||
|
||||
/** @var Timesheet $result */
|
||||
$result = $sut->getEntries()[0];
|
||||
$this->assertEquals('project description', $result->getDescription());
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,63 @@ class DurationCalculatorTest extends TestCase
|
||||
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 (second duration will not be rounded)
|
||||
8280
|
||||
],
|
||||
[
|
||||
[
|
||||
'default' => [
|
||||
'days' => [$day],
|
||||
'begin' => 0,
|
||||
'end' => 0,
|
||||
'duration' => 1,
|
||||
],
|
||||
'weekdays' => [
|
||||
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
||||
'begin' => 0,
|
||||
'end' => 0,
|
||||
'duration' => 1,
|
||||
],
|
||||
],
|
||||
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
|
||||
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
|
||||
0
|
||||
],
|
||||
[
|
||||
[
|
||||
'default' => [
|
||||
'days' => [$day],
|
||||
'begin' => 1,
|
||||
'end' => 1,
|
||||
'duration' => 1,
|
||||
],
|
||||
'weekdays' => [
|
||||
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
||||
'begin' => 1,
|
||||
'end' => 1,
|
||||
'duration' => 1,
|
||||
],
|
||||
],
|
||||
(clone $start)->setTime(12, 27, 00), // no diff, to test ...
|
||||
(clone $start)->setTime(12, 27, 00), // ... that no rounding is applied
|
||||
0
|
||||
],
|
||||
[
|
||||
[
|
||||
'default' => [
|
||||
'days' => [$day],
|
||||
'begin' => 0,
|
||||
'end' => 0,
|
||||
'duration' => 0,
|
||||
],
|
||||
'weekdays' => [
|
||||
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
||||
'begin' => 0,
|
||||
'end' => 0,
|
||||
'duration' => 0,
|
||||
],
|
||||
],
|
||||
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
|
||||
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
|
||||
0
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,5 +48,4 @@ class MarkdownExtensionTest extends TestCase
|
||||
$sut->timesheetContent("- test\n- foo\n\nfoo __bar__")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -730,6 +730,14 @@
|
||||
<source>invoice.service_date</source>
|
||||
<target>Leistungsdatum</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>Vorschau</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview_info">
|
||||
<source>invoice.preview_info</source>
|
||||
<target>Die folgenden Einträge werden in Ihrer Rechnung enthalten sein:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.amount">
|
||||
<source>invoice.amount</source>
|
||||
<target>Anzahl</target>
|
||||
@@ -738,6 +746,10 @@
|
||||
<source>invoice.total_rate</source>
|
||||
<target>Gesamtpreis</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.unit_price">
|
||||
<source>invoice.unit_price</source>
|
||||
<target>Einzelbetrag</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.payment_terms">
|
||||
<source>label.payment_terms</source>
|
||||
<target>Zahlungsinformationen</target>
|
||||
@@ -754,6 +766,10 @@
|
||||
<source>invoice_renderer.timesheet</source>
|
||||
<target>Stundenzettel</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_renderer.freelancer">
|
||||
<source>invoice_renderer.freelancer</source>
|
||||
<target>Freiberufler</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_number_generator">
|
||||
<source>label.invoice_number_generator</source>
|
||||
<target>Rechnungsnummern-Generator</target>
|
||||
@@ -782,10 +798,30 @@
|
||||
<source>invoice.signature_customer</source>
|
||||
<target>Leistungsbestätigung: Datum / Name Kunde / Unterschrift</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.total_working_time">
|
||||
<source>invoice.total_working_time</source>
|
||||
<target>Gesamtdauer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.order_number">
|
||||
<source>label.order_number</source>
|
||||
<target>Bestellnummer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_tax_number">
|
||||
<source>label.invoice_tax_number</source>
|
||||
<target>USt-IdNr.:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_bank_account">
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bankverbindung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Sehr geehrte Damen und Herren,
|
||||
|
||||
für Ihren Auftrag bedanke ich mich und berechne für meine Leistungen:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Month names
|
||||
|
||||
@@ -116,14 +116,6 @@
|
||||
<target>User</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Footer
|
||||
-->
|
||||
<trans-unit id="footer.license">
|
||||
<source>footer.license</source>
|
||||
<target>All rights reserved.</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Error templates
|
||||
-->
|
||||
@@ -738,6 +730,14 @@
|
||||
<source>invoice.service_date</source>
|
||||
<target>Service date</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview">
|
||||
<source>invoice.preview</source>
|
||||
<target>Preview</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.preview_info">
|
||||
<source>invoice.preview_info</source>
|
||||
<target>The following entries will be included in your invoice:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.amount">
|
||||
<source>invoice.amount</source>
|
||||
<target>Quantity</target>
|
||||
@@ -746,6 +746,10 @@
|
||||
<source>invoice.total_rate</source>
|
||||
<target>Total price</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.unit_price">
|
||||
<source>invoice.unit_price</source>
|
||||
<target>Unit price</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.payment_terms">
|
||||
<source>label.payment_terms</source>
|
||||
<target>Terms of payment</target>
|
||||
@@ -762,6 +766,10 @@
|
||||
<source>invoice_renderer.timesheet</source>
|
||||
<target>Timesheet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_renderer.freelancer">
|
||||
<source>invoice_renderer.freelancer</source>
|
||||
<target>Freelancer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_number_generator">
|
||||
<source>label.invoice_number_generator</source>
|
||||
<target>Invoicenumber-Generator</target>
|
||||
@@ -790,10 +798,30 @@
|
||||
<source>invoice.signature_customer</source>
|
||||
<target>Confirmation: Date / Name Customer / Signature</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.total_working_time">
|
||||
<source>invoice.total_working_time</source>
|
||||
<target>Total duration</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.order_number">
|
||||
<source>label.order_number</source>
|
||||
<target>Order number</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_tax_number">
|
||||
<source>label.invoice_tax_number</source>
|
||||
<target>VAT no.:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_bank_account">
|
||||
<source>label.invoice_bank_account</source>
|
||||
<target>Bank account</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_salutation">
|
||||
<source>label.invoice_salutation</source>
|
||||
<target>
|
||||
Ladies and gentlemen,
|
||||
|
||||
I thank you for your order and charge for my services:
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Month names
|
||||
|
||||
@@ -17,7 +17,7 @@ Encore
|
||||
.enableVersioning(Encore.isProduction())
|
||||
|
||||
// generate only two files: app.js and app.css
|
||||
.addEntry('app', './assets/js/app.js')
|
||||
.addEntry('app', './assets/app.js')
|
||||
|
||||
// enable sass/scss parser
|
||||
.enableSassLoader()
|
||||
|
||||
Reference in New Issue
Block a user