added new invoice fields, improved invoice templates (#1258)
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,6 +22,8 @@ templates/invoice/renderer/.~lock*
|
||||
|
||||
/var/invoices/*
|
||||
|
||||
/var/packages/*
|
||||
!var/packages/.gitkeep
|
||||
/var/plugins/*
|
||||
!var/plugins/.gitkeep
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
|
||||
## [1.6](https://github.com/kevinpapst/kimai2/releases/tag/1.6)
|
||||
|
||||
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates) - if you use them, add them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html))
|
||||
- Added new invoice fields (VAT, bank account) and customer fields (VAT), check if you want to update your settings
|
||||
- New functionality to manage permissions via Admin UI were added. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html)
|
||||
- New database tables were created, don't forget to [run the migrations](https://www.kimai.org/documentation/updates.html)
|
||||
|
||||
|
||||
@@ -197,6 +197,10 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
|
||||
err = '[' + xhr.status +'] ' + xhr.statusText;
|
||||
}
|
||||
alert.error(message, err);
|
||||
// this is useful for changing form fields and retrying to save (and in development to test form changes)
|
||||
setTimeout(function() {
|
||||
btn.button('reset');
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,301 +5,284 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.invoice {
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
body.invoice_print {
|
||||
background-color: #eee;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.invoice {
|
||||
margin: 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: 15px/1 $font-family-sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
||||
h1 {
|
||||
font: bold 100% sans-serif;
|
||||
letter-spacing: 0.5em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
margin: 105px auto 30px auto;
|
||||
padding: 50px 65px;
|
||||
min-height: 297mm;
|
||||
position: relative;
|
||||
max-width: 210mm;
|
||||
box-shadow: 0 0 20px rgba(80,80,80,0.7);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font: bold 1.5em sans-serif;
|
||||
margin-bottom: 1em;
|
||||
.page-header h3 {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
/* table */
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
div.invoice-address {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table.invoice-meta th {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
/* header */
|
||||
|
||||
header {
|
||||
table.invoice-sum th {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
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;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
article.address {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
article.footer {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/* article */
|
||||
|
||||
article, table.meta, table.inventory {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
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: 85%;
|
||||
|
||||
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) {
|
||||
table.invoice-sum th, table.invoice-sum td {
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
/* footer */
|
||||
.invoice-items {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 3em;
|
||||
|
||||
footer {
|
||||
.table {
|
||||
thead th {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 15px
|
||||
}
|
||||
tfoot {
|
||||
border-top: 1px solid #ddd;
|
||||
|
||||
tr:first-child th,
|
||||
tr:first-child td {
|
||||
padding-top: 15px
|
||||
}
|
||||
tr {
|
||||
th,td {
|
||||
border:none;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid #ccc;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
footer table {
|
||||
border-top: 1px solid #000;
|
||||
font-size: 0.8em;
|
||||
padding-top: 5px;
|
||||
padding-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
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, .wrapper, #freelancer-invoice {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* ================================ FREELANCER INVOICE ================================ */
|
||||
|
||||
#freelancer-invoice {
|
||||
margin: 1em 2em;
|
||||
|
||||
span:empty {
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
h1 {
|
||||
font: bold 100% sans-serif;
|
||||
letter-spacing: 0.5em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.add, .cut {
|
||||
display: none;
|
||||
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 {
|
||||
&:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
address {
|
||||
float: left;
|
||||
font-style: normal;
|
||||
line-height: 1.25;
|
||||
margin: 0 1em 1em 0;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
|
||||
article address {
|
||||
font-size: 1em;
|
||||
float: left;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
article.address {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
/* article */
|
||||
|
||||
article, table.meta, table.inventory {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
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: 85%;
|
||||
|
||||
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;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* table items */
|
||||
|
||||
table.inventory {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
margin-top: 2em;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
table.inventory th,
|
||||
table.inventory td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table.inventory thead th {
|
||||
font-weight: bold;
|
||||
border-width: 0 0 1px 0;
|
||||
border-bottom: 1px solid #000;
|
||||
padding-bottom: 15px
|
||||
}
|
||||
|
||||
table.inventory tbody tr:first-child td {
|
||||
padding-top: 15px
|
||||
}
|
||||
|
||||
table.inventory tbody tr:last-child td {
|
||||
padding-bottom: 15px
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(1),
|
||||
table.inventory th:nth-child(1) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(2),
|
||||
table.inventory th:nth-child(2) {
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(3),
|
||||
table.inventory th:nth-child(3) {
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(4),
|
||||
table.inventory th:nth-child(4) {
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: unset;
|
||||
/* ================================ PRINTING RULES ================================ */
|
||||
|
||||
@media print {
|
||||
* {
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
body, .wrapper {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: white;
|
||||
}
|
||||
#freelancer-invoice {
|
||||
span:empty {
|
||||
display: none;
|
||||
}
|
||||
.add, .cut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.invoice {
|
||||
margin: 1em 2em;
|
||||
font-size: 90%;
|
||||
width: unset;
|
||||
padding: 0;
|
||||
min-height: unset;
|
||||
position: unset;
|
||||
max-width: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
"symfony/thanks": "^1.0",
|
||||
"symfony/web-server-bundle": "^4.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "artifact",
|
||||
"url": "var/packages/"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.2"
|
||||
|
||||
1393
composer.lock
generated
1393
composer.lock
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,10 +5,10 @@
|
||||
"build/runtime.4ee6be68.js",
|
||||
"build/0.a87622f3.js",
|
||||
"build/1.c1bee41f.js",
|
||||
"build/app.120bf1d1.js"
|
||||
"build/app.c36bdaf3.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.ca6a8b7f.css"
|
||||
"build/app.e5eb271e.css"
|
||||
]
|
||||
},
|
||||
"chart": {
|
||||
@@ -35,8 +35,8 @@
|
||||
"build/runtime.4ee6be68.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||
"build/0.a87622f3.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok",
|
||||
"build/1.c1bee41f.js": "sha384-7UVWcP6Hefp2k/CrtGSITKXx4dSZqtvpAiU8WX7dClETkzMewrUjoCRtVXQ5j3KI",
|
||||
"build/app.120bf1d1.js": "sha384-2Eohdb33hChqGmyJIUGgZFiqzPfUDIv74IoMAX+09VozhJDD/GtPIg5+N2CCOACl",
|
||||
"build/app.ca6a8b7f.css": "sha384-NrITZnNaaIkzEjZkOjxciIbq6V6dKC9VlbJfxliLc2vi8n0113/yLfhbwi5rjzg2",
|
||||
"build/app.c36bdaf3.js": "sha384-lU6L1apGsiE2oAJfhqW7YaQo+J1eA5ysuOCfERdfGp63Z9ouvhQ9Ov3DlN+6aYXK",
|
||||
"build/app.e5eb271e.css": "sha384-BNUNuu56X2x+Xmn1JrQe6d3mt6R+QZu7OgR/4OXkBDueSE6vu0UqmRaDPkyiRzMw",
|
||||
"build/2.7be60d8d.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q",
|
||||
"build/chart.0af3f813.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
"build/calendar.6f94d071.js": "sha384-CnbCkgF1mbGOycK2A78CF1ZsQ4O07ol9U4gB0Tiec0wKu1mNw8lE22lvoRNzYs/8",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"build/0.a87622f3.js": "build/0.a87622f3.js",
|
||||
"build/1.c1bee41f.js": "build/1.c1bee41f.js",
|
||||
"build/2.7be60d8d.js": "build/2.7be60d8d.js",
|
||||
"build/app.css": "build/app.ca6a8b7f.css",
|
||||
"build/app.js": "build/app.120bf1d1.js",
|
||||
"build/app.css": "build/app.e5eb271e.css",
|
||||
"build/app.js": "build/app.c36bdaf3.js",
|
||||
"build/calendar.css": "build/calendar.36ef2a02.css",
|
||||
"build/calendar.js": "build/calendar.6f94d071.js",
|
||||
"build/chart.js": "build/chart.0af3f813.js",
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '1.5';
|
||||
public const VERSION = '1.6';
|
||||
/**
|
||||
* The current release status, either "stable" or "dev"
|
||||
*/
|
||||
public const STATUS = 'stable';
|
||||
public const STATUS = 'dev';
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -302,10 +302,6 @@ class ProfileController extends AbstractController
|
||||
|
||||
private function createPreferencesForm(User $user): FormInterface
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($user);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $this->createForm(
|
||||
UserPreferencesForm::class,
|
||||
$user,
|
||||
|
||||
@@ -46,7 +46,7 @@ class CustomerFixtures extends Fixture
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$faker = Factory::create();
|
||||
$faker = Factory::create('at_AT');
|
||||
|
||||
$amountCustomers = rand(self::MIN_CUSTOMERS, self::MAX_CUSTOMERS);
|
||||
for ($c = 1; $c <= $amountCustomers; $c++) {
|
||||
@@ -100,6 +100,7 @@ class CustomerFixtures extends Fixture
|
||||
->setCountry($faker->countryCode)
|
||||
->setTimezone($faker->timezone)
|
||||
->setVisible($visible)
|
||||
->setVatId($faker->vat)
|
||||
;
|
||||
|
||||
if (rand(0, 3) % 3) {
|
||||
|
||||
@@ -31,7 +31,9 @@ class InvoiceFixtures extends Fixture
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
foreach ($this->getInvoiceConfigs() as $invoiceConfig) {
|
||||
$faker = Factory::create('at_AT');
|
||||
|
||||
foreach ($this->getInvoiceConfigs($faker) as $invoiceConfig) {
|
||||
$template = new InvoiceTemplate();
|
||||
|
||||
// name, title, renderer, calculator, numberGenerator, company, vat, dueDays, address, paymentTerms
|
||||
@@ -44,8 +46,11 @@ class InvoiceFixtures extends Fixture
|
||||
->setCompany($invoiceConfig[5])
|
||||
->setVat($invoiceConfig[6])
|
||||
->setDueDays($invoiceConfig[7])
|
||||
->setAddress($invoiceConfig[8])
|
||||
->setPaymentTerms($invoiceConfig[9])
|
||||
->setPaymentTerms($invoiceConfig[8])
|
||||
->setVatId($faker->vat)
|
||||
->setAddress($this->generateAddress($faker))
|
||||
->setContact($this->generateContact($faker))
|
||||
->setPaymentDetails($this->generatePaymentDetails($faker))
|
||||
;
|
||||
|
||||
$manager->persist($template);
|
||||
@@ -53,10 +58,8 @@ class InvoiceFixtures extends Fixture
|
||||
}
|
||||
}
|
||||
|
||||
private function getInvoiceConfigs()
|
||||
private function getInvoiceConfigs(Generator $faker)
|
||||
{
|
||||
$faker = Factory::create();
|
||||
|
||||
$paymentTerms =
|
||||
'I would like to thank you for your confidence and will gladly be there for you in the future.' .
|
||||
PHP_EOL .
|
||||
@@ -64,11 +67,9 @@ class InvoiceFixtures extends Fixture
|
||||
'as reference.'
|
||||
;
|
||||
|
||||
$address =
|
||||
$faker->streetAddress . PHP_EOL .
|
||||
$faker->city . ', ' . $faker->stateAbbr . ' ' . $faker->postcode . PHP_EOL .
|
||||
'Phone: ' . $faker->phoneNumber . PHP_EOL .
|
||||
'Email: ' . $faker->safeEmail
|
||||
$paymentTerms_alt =
|
||||
$faker->firstName . ', thank you very much. We really appreciate your business.' . PHP_EOL .
|
||||
'Please send payments before the due date.I would like to thank you for your confidence and will gladly be there for you in the future.'
|
||||
;
|
||||
|
||||
$paymentTerms_de =
|
||||
@@ -85,17 +86,14 @@ class InvoiceFixtures extends Fixture
|
||||
|
||||
// name, title, renderer, calculator, numberGenerator, company, vat, dueDays, address, paymentTerms
|
||||
return [
|
||||
['Invoice (HTML)', 'Company name', 'default', 'default', 'default', $faker->company, 19, 30, $address, $paymentTerms],
|
||||
['Freelancer (HTML, short)', 'Invoice', 'freelancer', 'short', 'default', $faker->company, 19, 14, $this->generateAddress($faker), $paymentTerms_de],
|
||||
['Timesheet (HTML)', 'Timesheet', 'timesheet', 'default', 'default', $faker->company, 19, 7, $this->generateAddress($faker), ''],
|
||||
['Company invoice (DOCX)', 'Invoice', 'company', 'default', 'default', 'Kimai Inc.', 19, 14, $this->generateAddress($faker, true), $this->generatePaymentTerms($faker)],
|
||||
['Export (CSV, user-group)', 'User-grouped', 'export', 'user', 'default', $faker->company, 7, 28, '', ''],
|
||||
['Export (ODS)', 'Spreadsheet', 'open-spreadsheet', 'default', 'default', $faker->company, 19, 14, '', ''],
|
||||
['Export (XLSX, user-group)', 'Spreadsheet', 'spreadsheet', 'user', 'default', $faker->company, 13, 10, '', ''],
|
||||
['Invoice (HTML)', 'Company name', 'default', 'default', 'default', $faker->company, 19, 30, $paymentTerms],
|
||||
['Freelancer (HTML, short)', 'Invoice', 'freelancer', 'short', 'default', $faker->company, 19, 14, $paymentTerms_de],
|
||||
['Timesheet (HTML)', 'Timesheet', 'timesheet', 'default', 'default', $faker->company, 19, 7, $paymentTerms_alt],
|
||||
['Company invoice (DOCX)', 'Invoice', 'company', 'default', 'default', 'Kimai Inc.', 19, 14, $paymentTerms_alt],
|
||||
];
|
||||
}
|
||||
|
||||
protected function generatePaymentTerms(Generator $faker)
|
||||
protected function generatePaymentDetails(Generator $faker)
|
||||
{
|
||||
return
|
||||
'Acme Bank' . PHP_EOL .
|
||||
@@ -104,15 +102,17 @@ class InvoiceFixtures extends Fixture
|
||||
;
|
||||
}
|
||||
|
||||
protected function generateAddress(Generator $faker, $lineBreaks = false)
|
||||
protected function generateContact(Generator $faker)
|
||||
{
|
||||
if (!$lineBreaks) {
|
||||
return
|
||||
$faker->name . ' - ' .
|
||||
$faker->streetAddress . '-' .
|
||||
$faker->postcode . ' ' . $faker->city;
|
||||
}
|
||||
return
|
||||
'Phone: ' . $faker->phoneNumber . PHP_EOL .
|
||||
'Email: ' . $faker->safeEmail . PHP_EOL .
|
||||
'Web: www.' . $faker->domainName
|
||||
;
|
||||
}
|
||||
|
||||
protected function generateAddress(Generator $faker)
|
||||
{
|
||||
return
|
||||
'Kimai Inc.' . PHP_EOL .
|
||||
$faker->streetAddress . PHP_EOL .
|
||||
|
||||
@@ -55,7 +55,7 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
/**
|
||||
* Whether we should deactivate foreign key support for SQLite.
|
||||
* This is required, if columns are changed.
|
||||
* SQLite will drop the table and therefor all referenced data if we don't deactivate this.
|
||||
* SQLite will drop the table and all referenced data, if we don't deactivate this.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
@@ -79,6 +79,14 @@ class Customer implements EntityWithMetaFields
|
||||
*/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="vat_id", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $vatId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
@@ -272,6 +280,18 @@ class Customer implements EntityWithMetaFields
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
public function getVatId(): ?string
|
||||
{
|
||||
return $this->vatId;
|
||||
}
|
||||
|
||||
public function setVatId(?string $vatId): Customer
|
||||
{
|
||||
$this->vatId = $vatId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setContact(?string $contact): Customer
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
@@ -56,6 +56,14 @@ class InvoiceTemplate
|
||||
*/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="vat_id", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $vatId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
@@ -63,6 +71,13 @@ class InvoiceTemplate
|
||||
*/
|
||||
private $address;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="contact", type="text", nullable=true)
|
||||
*/
|
||||
private $contact;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
@@ -109,6 +124,13 @@ class InvoiceTemplate
|
||||
*/
|
||||
private $paymentTerms;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="payment_details", type="text", nullable=true)
|
||||
*/
|
||||
private $paymentDetails;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -234,6 +256,42 @@ class InvoiceTemplate
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVatId(): ?string
|
||||
{
|
||||
return $this->vatId;
|
||||
}
|
||||
|
||||
public function setVatId(?string $vatId): InvoiceTemplate
|
||||
{
|
||||
$this->vatId = $vatId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContact(): ?string
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
public function setContact(?string $contact): InvoiceTemplate
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPaymentDetails(): ?string
|
||||
{
|
||||
return $this->paymentDetails;
|
||||
}
|
||||
|
||||
public function setPaymentDetails(?string $paymentDetails): InvoiceTemplate
|
||||
{
|
||||
$this->paymentDetails = $paymentDetails;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -71,9 +71,9 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_invoice')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], $this->getIcon('invoice'))
|
||||
);
|
||||
$invoice = new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], $this->getIcon('invoice'));
|
||||
$invoice->setChildRoutes(['admin_invoice_template', 'admin_invoice_template_edit', 'admin_invoice_template_create']);
|
||||
$menu->addItem($invoice);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('create_export')) {
|
||||
|
||||
@@ -56,6 +56,10 @@ class CustomerEditForm extends AbstractType
|
||||
'label' => 'label.company',
|
||||
'required' => false,
|
||||
])
|
||||
->add('vatId', TextType::class, [
|
||||
'label' => 'label.vat_id',
|
||||
'required' => false,
|
||||
])
|
||||
->add('contact', TextType::class, [
|
||||
'label' => 'label.contact',
|
||||
'required' => false,
|
||||
|
||||
@@ -40,14 +40,26 @@ class InvoiceTemplateForm extends AbstractType
|
||||
->add('company', TextType::class, [
|
||||
'label' => 'label.company',
|
||||
])
|
||||
->add('vatId', TextType::class, [
|
||||
'label' => 'label.vat_id',
|
||||
'required' => false,
|
||||
])
|
||||
->add('address', TextareaType::class, [
|
||||
'label' => 'label.address',
|
||||
'required' => false,
|
||||
])
|
||||
->add('contact', TextareaType::class, [
|
||||
'label' => 'label.contact',
|
||||
'required' => false,
|
||||
])
|
||||
->add('paymentTerms', TextareaType::class, [
|
||||
'label' => 'label.payment_terms',
|
||||
'required' => false,
|
||||
])
|
||||
->add('paymentDetails', TextareaType::class, [
|
||||
'label' => 'label.invoice_bank_account',
|
||||
'required' => false,
|
||||
])
|
||||
->add('dueDays', TextType::class, [
|
||||
'label' => 'label.due_days',
|
||||
])
|
||||
@@ -71,6 +83,11 @@ class InvoiceTemplateForm extends AbstractType
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_invoice_template',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.invoiceTemplateUpdate',
|
||||
'data-msg-success' => 'action.update.success',
|
||||
'data-msg-error' => 'action.update.error',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class InvoiceToolbarForm extends AbstractToolbarForm
|
||||
]);
|
||||
$builder->add('create', SubmitType::class, [
|
||||
'label' => 'button.print',
|
||||
'attr' => ['formtarget' => 'invoice'],
|
||||
'attr' => ['formtarget' => '_blank'],
|
||||
]);
|
||||
$builder->add('preview', SubmitType::class, [
|
||||
'label' => 'button.preview',
|
||||
|
||||
@@ -50,6 +50,7 @@ class InvoiceCalculatorType extends AbstractType
|
||||
return $renderer;
|
||||
},
|
||||
'translation_domain' => 'invoice-calculator',
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ class InvoiceNumberGeneratorType extends AbstractType
|
||||
return $renderer;
|
||||
},
|
||||
'translation_domain' => 'invoice-numbergenerator',
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ class InvoiceRendererType extends AbstractType
|
||||
},
|
||||
'translation_domain' => 'invoice-renderer',
|
||||
'docu_chapter' => 'invoices.html',
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class YesNoType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'value' => true,
|
||||
'false_values' => [null, 0, false, 'false'],
|
||||
'false_values' => [null, 0, false, 'false', '', '0'],
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,9 @@ trait RendererTrait
|
||||
'template.title' => $model->getTemplate()->getTitle(),
|
||||
'template.payment_terms' => $model->getTemplate()->getPaymentTerms(),
|
||||
'template.due_days' => $model->getTemplate()->getDueDays(),
|
||||
'template.vat_id' => $model->getTemplate()->getVatId(),
|
||||
'template.contact' => $model->getTemplate()->getContact(),
|
||||
'template.payment_details' => $model->getTemplate()->getPaymentDetails(),
|
||||
|
||||
'query.begin' => $this->getFormattedDateTime($model->getQuery()->getBegin()),
|
||||
'query.day' => $model->getQuery()->getBegin()->format('d'),
|
||||
@@ -164,6 +167,7 @@ trait RendererTrait
|
||||
'customer.name' => $customer->getName(),
|
||||
'customer.contact' => $customer->getContact(),
|
||||
'customer.company' => $customer->getCompany(),
|
||||
'customer.vat' => $customer->getVatId(),
|
||||
'customer.number' => $customer->getNumber(),
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
|
||||
@@ -15,35 +15,23 @@ use App\Invoice\RendererInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
|
||||
class TwigRenderer implements RendererInterface
|
||||
final class TwigRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
protected $twig;
|
||||
private $twig;
|
||||
|
||||
/**
|
||||
* @param Environment $twig
|
||||
*/
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceDocument $document
|
||||
* @return bool
|
||||
*/
|
||||
public function supports(InvoiceDocument $document): bool
|
||||
{
|
||||
return stripos($document->getFilename(), '.twig') !== false;
|
||||
return stripos($document->getFilename(), '.html.twig') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceDocument $document
|
||||
* @param InvoiceModel $model
|
||||
* @return Response
|
||||
*/
|
||||
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
||||
{
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
|
||||
67
src/Migrations/Version20191116110124.php
Normal file
67
src/Migrations/Version20191116110124.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* New Vat ID columns and invoice template improvements
|
||||
*
|
||||
* @version 1.6
|
||||
*/
|
||||
final class Version20191116110124 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'New Vat ID columns and invoice template improvements';
|
||||
}
|
||||
|
||||
protected function isSupportingForeignKeys(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
if ($this->isPlatformSqlite()) {
|
||||
// does fail if we use transactions, as tables are re-created and foreign keys would fail
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$customers = $schema->getTable('kimai2_customers');
|
||||
$customers->addColumn('vat_id', 'string', ['length' => 50, 'notnull' => false]);
|
||||
|
||||
$invoiceTemplates = $schema->getTable('kimai2_invoice_templates');
|
||||
$invoiceTemplates->addColumn('vat_id', 'string', ['length' => 50, 'notnull' => false, 'default' => null]);
|
||||
$invoiceTemplates->addColumn('contact', 'text', ['notnull' => false, 'default' => null]);
|
||||
$invoiceTemplates->addColumn('payment_details', 'text', ['notnull' => false, 'default' => null]);
|
||||
|
||||
$this->addSql("UPDATE kimai2_invoice_templates SET renderer = 'default' WHERE renderer IN ('export', 'open-spreadsheet', 'spreadsheet')");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$invoiceTemplates = $schema->getTable('kimai2_invoice_templates');
|
||||
$invoiceTemplates->dropColumn('payment_details');
|
||||
$invoiceTemplates->dropColumn('contact');
|
||||
$invoiceTemplates->dropColumn('vat_id');
|
||||
|
||||
$customers = $schema->getTable('kimai2_customers');
|
||||
$customers->dropColumn('vat_id');
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,8 @@ class CustomerRepository extends EntityRepository
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->like('c.name', ':searchTerm'),
|
||||
$qb->expr()->like('c.comment', ':searchTerm'),
|
||||
$qb->expr()->like('c.company', ':searchTerm'),
|
||||
$qb->expr()->like('c.vatId', ':searchTerm'),
|
||||
$qb->expr()->like('c.number', ':searchTerm'),
|
||||
$qb->expr()->like('c.contact', ':searchTerm'),
|
||||
$qb->expr()->like('c.phone', ':searchTerm'),
|
||||
|
||||
@@ -25,7 +25,14 @@
|
||||
{{ form_row(form.comment) }}
|
||||
<fieldset>
|
||||
{#<legend>Company data</legend>#}
|
||||
{{ form_row(form.company) }}
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{{ form_row(form.company) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.vatId) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_row(form.address) }}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
|
||||
@@ -10,56 +10,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row invoice-info">
|
||||
<div class="col-sm-4 invoice-col">
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
{{ 'invoice.from'|trans }}
|
||||
<address contenteditable="true">
|
||||
<strong>{{ model.template.company }}</strong><br>
|
||||
{{ model.template.address|trim|nl2br }}
|
||||
{% if model.template.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans }}:
|
||||
{{ model.template.vatId }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</div>
|
||||
<div class="col-sm-4 invoice-col">
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-5">
|
||||
{% set customerPhone = model.customer.phone|default(model.customer.mobile) %}
|
||||
{{ 'invoice.to'|trans }}
|
||||
<address contenteditable="true">
|
||||
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
|
||||
{{ model.customer.address|nl2br }}
|
||||
{% if model.customer.phone is not empty %}
|
||||
{% if model.customer.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.phone'|trans }}: {{ model.customer.phone }}
|
||||
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
|
||||
{% endif %}
|
||||
{% if model.customer.email is not empty %}
|
||||
<br>
|
||||
{{ 'label.email'|trans }}: {{ model.customer.email }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</div>
|
||||
<div class="col-sm-4 invoice-col">
|
||||
<p contenteditable="true">
|
||||
<b>{{ 'invoice.number'|trans }}: {{ model.numberGenerator.invoiceNumber }}</b>
|
||||
</p>
|
||||
<p contenteditable="true">
|
||||
<b>{{ 'invoice.due_days'|trans }}:</b> {{ model.dueDate|date_short }}
|
||||
{% if model.customer.number is not empty %}
|
||||
<br><b>{{ 'label.number'|trans }}:</b> {{ model.customer.number }}
|
||||
<br>
|
||||
{{ 'label.number'|trans }}: {{ model.customer.number }}
|
||||
{% endif %}
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<br><b>{{ 'label.orderNumber'|trans }}:</b> {{ model.query.project.orderNumber }}
|
||||
<br>
|
||||
{{ 'label.orderNumber'|trans }}: {{ model.query.project.orderNumber }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
<p contenteditable="true">
|
||||
<strong>{{ 'invoice.number'|trans }}:</strong>
|
||||
{{ model.numberGenerator.invoiceNumber }}
|
||||
|
||||
<br>
|
||||
<strong>{{ 'invoice.due_days'|trans }}:</strong>
|
||||
{{ model.dueDate|date_short }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-7"></div>
|
||||
</div>
|
||||
|
||||
<div class="row invoice-items">
|
||||
<div class="col-xs-12 table-responsive">
|
||||
<table class="table table-striped">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}</th>
|
||||
<th>{{ 'label.description'|trans }}</th>
|
||||
<th>{{ 'label.unit_price'|trans }}</th>
|
||||
<th>{{ 'label.hours'|trans }}</th>
|
||||
<th>{{ 'label.total_rate'|trans }}</th>
|
||||
<th class="text-center">{{ 'label.hours'|trans }}</th>
|
||||
<th class="text-right">{{ 'label.total_rate'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -74,7 +84,7 @@
|
||||
{% set rate = app.user.getPreferenceValue('hourly_rate') %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{{ entry.begin|date_short }}</td>
|
||||
<td nowrap class="text-nowrap">{{ entry.begin|date_short }}</td>
|
||||
<td contenteditable="true">
|
||||
{% if entry.description is not empty %}
|
||||
{{ entry.description|nl2br }}
|
||||
@@ -82,48 +92,56 @@
|
||||
{{ entry.activity.name }} / {{ entry.project.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td contenteditable="true">{{ rate|money(model.calculator.currency) }}</td>
|
||||
<td>{{ duration }}</td>
|
||||
<td>{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
<td nowrap class="text-nowrap" contenteditable="true">{{ rate|money(model.calculator.currency) }}</td>
|
||||
<td nowrap class="text-nowrap text-center">{{ duration }}</td>
|
||||
<td nowrap class="text-nowrap text-right">{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
{{ 'invoice.subtotal'|trans }}
|
||||
</td>
|
||||
<td class="text-right">{{ model.calculator.subtotal|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
|
||||
</td>
|
||||
<td class="text-right">{{ model.calculator.tax|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
<strong>{{ 'invoice.total'|trans }}</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<strong>{{ model.calculator.total|money(model.calculator.currency) }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="col-xs-12">
|
||||
{% if model.template.paymentTerms is not empty %}
|
||||
<p class="lead">{{ 'label.payment_terms'|trans }}</p>
|
||||
|
||||
<p class="text-muted well well-sm no-shadow" contenteditable="true">
|
||||
{{ model.template.paymentTerms|trim|nl2br }}
|
||||
<p contenteditable="true" class="paymentTerms">
|
||||
{{ model.template.paymentTerms|nl2br|md2html }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-xs-6">
|
||||
<p class="lead" contenteditable="true">{{ 'invoice.due_days'|trans }} {{ model.dueDate|date_short }}</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:50%">{{ '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>{{ 'invoice.total'|trans }}</th>
|
||||
<td>{{ model.calculator.total|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>
|
||||
<strong>{{ 'label.address'|trans }}</strong>: {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<br>
|
||||
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<br>
|
||||
<strong>{{ 'label.contact'|trans }}</strong>: {{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
{% endblock %}
|
||||
@@ -4,30 +4,18 @@
|
||||
|
||||
{% block invoice %}
|
||||
|
||||
{# You can overwrite 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)
|
||||
'
|
||||
}) %}
|
||||
{% set signature = null %}
|
||||
{# You can overwrite these settings in your config/packages/local.yaml under the key "twig.globals.company" - previously '/build/images/signature.png' #}
|
||||
{% set company = company|default({signature: null}) %}
|
||||
{% if company.signature is defined and company.signature is not empty %}
|
||||
{% set signature = company.signature %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row" id="freelancer-invoice" style="page:invoiceFreelancer;">
|
||||
<div class="row" id="freelancer-invoice">
|
||||
<div class="col-xs-12">
|
||||
<header>
|
||||
<address>
|
||||
<p contenteditable="true">{{ model.template.address }}</p>
|
||||
<p contenteditable="true">{{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}</p>
|
||||
</address>
|
||||
</header>
|
||||
<article class="address">
|
||||
@@ -36,6 +24,10 @@
|
||||
<p>
|
||||
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
|
||||
{{ model.customer.address|nl2br }}
|
||||
{% if model.customer.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</address>
|
||||
<table class="meta">
|
||||
@@ -59,6 +51,14 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if model.template.vatId is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.vat_id'|trans }}:</th>
|
||||
<td>
|
||||
{{ model.template.vatId }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</article>
|
||||
<article>
|
||||
@@ -118,44 +118,44 @@
|
||||
</table>
|
||||
</article>
|
||||
{% if model.template.paymentTerms is not empty %}
|
||||
<article class="footer">
|
||||
<article class="paymentTerms">
|
||||
<p contenteditable="true">
|
||||
{{ model.template.paymentTerms|trim|nl2br }}
|
||||
{{ model.template.paymentTerms|nl2br|md2html }}
|
||||
</p>
|
||||
{% if company.signature is defined %}
|
||||
<img src="{{ company.signature }}">
|
||||
{% if signature is not empty %}
|
||||
<img src="{{ 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 class="footer">
|
||||
<div class="row">
|
||||
<div class="col-sm-4" contenteditable="true">
|
||||
<p>
|
||||
<strong>{{ 'label.address'|trans }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{{ model.template.address|nl2br }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-4 text-center" contenteditable="true">
|
||||
<p>
|
||||
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{{ model.template.paymentDetails|nl2br }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-4 text-right" contenteditable="true">
|
||||
<p>
|
||||
<strong>{{ 'label.contact'|trans }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{{ model.template.contact|nl2br }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row invoice-items">
|
||||
<div class="col-xs-12 table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
@@ -97,22 +97,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding: 60px 0">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{% if model.template.paymentTerms is not empty %}
|
||||
<p class="lead">{{ 'label.payment_terms'|trans }}</p>
|
||||
|
||||
<p class="text-muted well well-sm no-shadow" contenteditable="true">
|
||||
<p class="text-muted well well-sm no-shadow" contenteditable="true" style="margin-bottom: 100px">
|
||||
{{ model.template.paymentTerms|trim|nl2br }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:50%;padding-bottom: 60px">{{ 'invoice.signature_user'|trans }}</th>
|
||||
<th style="padding-bottom: 60px">{{ 'invoice.signature_user'|trans }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.signature_customer'|trans }}</th>
|
||||
|
||||
@@ -3,9 +3,54 @@
|
||||
{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
|
||||
{% set formOptions = {
|
||||
'title': template.name|default('create'|trans),
|
||||
'form': form,
|
||||
'back': path('admin_invoice_template')
|
||||
}) }}
|
||||
} %}
|
||||
|
||||
{% embed formEditTemplate with formOptions %}
|
||||
{% block form_body %}
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.title) }}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.company) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.vatId) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.address) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.contact) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.paymentTerms) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.paymentDetails) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.dueDays) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.vat) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_row(form.renderer) }}
|
||||
{{ form_row(form.calculator) }}
|
||||
{{ form_row(form.numberGenerator) }}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
{% set tableName = 'invoice_template' %}
|
||||
|
||||
{{ tables.data_table_header(tableName, columns) }}
|
||||
{{ tables.datatable_header(tableName, columns, null, {'reload': 'kimai.invoiceTemplateUpdate'}) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr class="modal-ajax-form open-edit" data-href="{{ path('admin_invoice_template_edit', {'id' : entry.id}) }}">
|
||||
|
||||
@@ -23,85 +23,89 @@ class CustomerTest extends TestCase
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = new Customer();
|
||||
$this->assertNull($sut->getId());
|
||||
$this->assertNull($sut->getName());
|
||||
$this->assertNull($sut->getNumber());
|
||||
$this->assertNull($sut->getComment());
|
||||
$this->assertTrue($sut->getVisible());
|
||||
self::assertNull($sut->getId());
|
||||
self::assertNull($sut->getName());
|
||||
self::assertNull($sut->getNumber());
|
||||
self::assertNull($sut->getComment());
|
||||
self::assertTrue($sut->getVisible());
|
||||
|
||||
$this->assertNull($sut->getCompany());
|
||||
$this->assertNull($sut->getContact());
|
||||
$this->assertNull($sut->getAddress());
|
||||
$this->assertNull($sut->getCountry());
|
||||
$this->assertEquals('EUR', $sut->getCurrency());
|
||||
$this->assertEquals('EUR', Customer::DEFAULT_CURRENCY);
|
||||
$this->assertNull($sut->getPhone());
|
||||
$this->assertNull($sut->getFax());
|
||||
$this->assertNull($sut->getMobile());
|
||||
$this->assertNull($sut->getEmail());
|
||||
$this->assertNull($sut->getHomepage());
|
||||
$this->assertNull($sut->getTimezone());
|
||||
self::assertNull($sut->getCompany());
|
||||
self::assertNull($sut->getVatId());
|
||||
self::assertNull($sut->getContact());
|
||||
self::assertNull($sut->getAddress());
|
||||
self::assertNull($sut->getCountry());
|
||||
self::assertEquals('EUR', $sut->getCurrency());
|
||||
self::assertEquals('EUR', Customer::DEFAULT_CURRENCY);
|
||||
self::assertNull($sut->getPhone());
|
||||
self::assertNull($sut->getFax());
|
||||
self::assertNull($sut->getMobile());
|
||||
self::assertNull($sut->getEmail());
|
||||
self::assertNull($sut->getHomepage());
|
||||
self::assertNull($sut->getTimezone());
|
||||
|
||||
$this->assertNull($sut->getFixedRate());
|
||||
$this->assertNull($sut->getHourlyRate());
|
||||
$this->assertNull($sut->getColor());
|
||||
$this->assertEquals(0.0, $sut->getBudget());
|
||||
$this->assertEquals(0, $sut->getTimeBudget());
|
||||
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
$this->assertEquals(0, $sut->getMetaFields()->count());
|
||||
$this->assertNull($sut->getMetaField('foo'));
|
||||
$this->assertInstanceOf(Collection::class, $sut->getTeams());
|
||||
$this->assertEquals(0, $sut->getTeams()->count());
|
||||
self::assertNull($sut->getFixedRate());
|
||||
self::assertNull($sut->getHourlyRate());
|
||||
self::assertNull($sut->getColor());
|
||||
self::assertEquals(0.0, $sut->getBudget());
|
||||
self::assertEquals(0, $sut->getTimeBudget());
|
||||
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
self::assertEquals(0, $sut->getMetaFields()->count());
|
||||
self::assertNull($sut->getMetaField('foo'));
|
||||
self::assertInstanceOf(Collection::class, $sut->getTeams());
|
||||
self::assertEquals(0, $sut->getTeams()->count());
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
{
|
||||
$sut = new Customer();
|
||||
$this->assertInstanceOf(Customer::class, $sut->setName('foo-bar'));
|
||||
$this->assertEquals('foo-bar', $sut->getName());
|
||||
$this->assertEquals('foo-bar', (string) $sut);
|
||||
self::assertInstanceOf(Customer::class, $sut->setName('foo-bar'));
|
||||
self::assertEquals('foo-bar', $sut->getName());
|
||||
self::assertEquals('foo-bar', (string) $sut);
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setVisible(false));
|
||||
$this->assertFalse($sut->getVisible());
|
||||
self::assertInstanceOf(Customer::class, $sut->setVisible(false));
|
||||
self::assertFalse($sut->getVisible());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setComment('hello world'));
|
||||
$this->assertEquals('hello world', $sut->getComment());
|
||||
self::assertInstanceOf(Customer::class, $sut->setComment('hello world'));
|
||||
self::assertEquals('hello world', $sut->getComment());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setColor('#fffccc'));
|
||||
$this->assertEquals('#fffccc', $sut->getColor());
|
||||
self::assertInstanceOf(Customer::class, $sut->setColor('#fffccc'));
|
||||
self::assertEquals('#fffccc', $sut->getColor());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setCompany('test company'));
|
||||
$this->assertEquals('test company', $sut->getCompany());
|
||||
self::assertInstanceOf(Customer::class, $sut->setCompany('test company'));
|
||||
self::assertEquals('test company', $sut->getCompany());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setContact('test contact'));
|
||||
$this->assertEquals('test contact', $sut->getContact());
|
||||
self::assertInstanceOf(Customer::class, $sut->setContact('test contact'));
|
||||
self::assertEquals('test contact', $sut->getContact());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setPhone('0123456789'));
|
||||
$this->assertEquals('0123456789', $sut->getPhone());
|
||||
self::assertInstanceOf(Customer::class, $sut->setPhone('0123456789'));
|
||||
self::assertEquals('0123456789', $sut->getPhone());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setFax('asdfghjkl'));
|
||||
$this->assertEquals('asdfghjkl', $sut->getFax());
|
||||
self::assertInstanceOf(Customer::class, $sut->setFax('asdfghjkl'));
|
||||
self::assertEquals('asdfghjkl', $sut->getFax());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setMobile('76576534'));
|
||||
$this->assertEquals('76576534', $sut->getMobile());
|
||||
self::assertInstanceOf(Customer::class, $sut->setMobile('76576534'));
|
||||
self::assertEquals('76576534', $sut->getMobile());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setEmail('test@example.com'));
|
||||
$this->assertEquals('test@example.com', $sut->getEmail());
|
||||
self::assertInstanceOf(Customer::class, $sut->setEmail('test@example.com'));
|
||||
self::assertEquals('test@example.com', $sut->getEmail());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setHomepage('https://www.example.com'));
|
||||
$this->assertEquals('https://www.example.com', $sut->getHomepage());
|
||||
self::assertInstanceOf(Customer::class, $sut->setHomepage('https://www.example.com'));
|
||||
self::assertEquals('https://www.example.com', $sut->getHomepage());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setFixedRate(13.47));
|
||||
$this->assertEquals(13.47, $sut->getFixedRate());
|
||||
self::assertInstanceOf(Customer::class, $sut->setFixedRate(13.47));
|
||||
self::assertEquals(13.47, $sut->getFixedRate());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setHourlyRate(99));
|
||||
$this->assertEquals(99, $sut->getHourlyRate());
|
||||
self::assertInstanceOf(Customer::class, $sut->setHourlyRate(99));
|
||||
self::assertEquals(99, $sut->getHourlyRate());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setBudget(12345.67));
|
||||
$this->assertEquals(12345.67, $sut->getBudget());
|
||||
self::assertInstanceOf(Customer::class, $sut->setBudget(12345.67));
|
||||
self::assertEquals(12345.67, $sut->getBudget());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setTimeBudget(937321));
|
||||
$this->assertEquals(937321, $sut->getTimeBudget());
|
||||
self::assertInstanceOf(Customer::class, $sut->setTimeBudget(937321));
|
||||
self::assertEquals(937321, $sut->getTimeBudget());
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setVatId('ID 1234567890'));
|
||||
self::assertEquals('ID 1234567890', $sut->getVatId());
|
||||
}
|
||||
|
||||
public function testMetaFields()
|
||||
@@ -109,7 +113,7 @@ class CustomerTest extends TestCase
|
||||
$sut = new Customer();
|
||||
$meta = new CustomerMeta();
|
||||
$meta->setName('foo')->setValue('bar')->setType('test');
|
||||
$this->assertInstanceOf(Customer::class, $sut->setMetaField($meta));
|
||||
self::assertInstanceOf(Customer::class, $sut->setMetaField($meta));
|
||||
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||
$result = $sut->getMetaField('foo');
|
||||
self::assertSame($result, $meta);
|
||||
@@ -117,7 +121,7 @@ class CustomerTest extends TestCase
|
||||
|
||||
$meta2 = new CustomerMeta();
|
||||
$meta2->setName('foo')->setValue('bar')->setType('test2');
|
||||
$this->assertInstanceOf(Customer::class, $sut->setMetaField($meta2));
|
||||
self::assertInstanceOf(Customer::class, $sut->setMetaField($meta2));
|
||||
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||
self::assertCount(0, $sut->getVisibleMetaFields());
|
||||
|
||||
|
||||
@@ -17,36 +17,63 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
class InvoiceTemplateTest extends TestCase
|
||||
{
|
||||
protected function assertIsFluent($actual)
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$this->assertInstanceOf(InvoiceTemplate::class, $actual);
|
||||
$sut = new InvoiceTemplate();
|
||||
|
||||
self::assertNull($sut->getVatId());
|
||||
self::assertNull($sut->getPaymentDetails());
|
||||
self::assertNull($sut->getPaymentTerms());
|
||||
self::assertNull($sut->getContact());
|
||||
self::assertEquals(0.00, $sut->getVat());
|
||||
self::assertNull($sut->getId());
|
||||
self::assertNull($sut->getName());
|
||||
self::assertNull($sut->getAddress());
|
||||
self::assertNull($sut->getTitle());
|
||||
self::assertNull($sut->getCompany());
|
||||
self::assertEquals('default', $sut->getCalculator());
|
||||
self::assertEquals('default', $sut->getNumberGenerator());
|
||||
self::assertEquals('default', $sut->getRenderer());
|
||||
self::assertEquals(30, $sut->getDueDays());
|
||||
}
|
||||
|
||||
protected function getEntity()
|
||||
public function testSetNullForOptionalValues()
|
||||
{
|
||||
$entity = new InvoiceTemplate();
|
||||
$sut = new InvoiceTemplate();
|
||||
|
||||
return $entity;
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentDetails(null));
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setVatId(null));
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setContact(null));
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setAddress(null));
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentTerms(null));
|
||||
}
|
||||
|
||||
public function testSetGetPaymentTerms()
|
||||
public function testSetterAndGetter()
|
||||
{
|
||||
$sut = $this->getEntity();
|
||||
$sut = new InvoiceTemplate();
|
||||
|
||||
$this->assertNull($sut->getPaymentTerms());
|
||||
$this->assertIsFluent($sut->setPaymentTerms(null));
|
||||
$this->assertIsFluent($sut->setPaymentTerms(''));
|
||||
$this->assertIsFluent($sut->setPaymentTerms('foo bar'));
|
||||
$this->assertEquals('foo bar', $sut->getPaymentTerms());
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentTerms('foo bar'));
|
||||
self::assertEquals('foo bar', $sut->getPaymentTerms());
|
||||
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentDetails('iuasdzgf isdfhvlksdjfbnvl ksdfbglisbdf'));
|
||||
self::assertEquals('iuasdzgf isdfhvlksdjfbnvl ksdfbglisbdf', $sut->getPaymentDetails());
|
||||
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setContact('hello world'));
|
||||
self::assertEquals('hello world', $sut->getContact());
|
||||
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setVat(7.31));
|
||||
self::assertEquals(7.31, $sut->getVat());
|
||||
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setVatId('1234567890'));
|
||||
self::assertEquals('1234567890', $sut->getVatId());
|
||||
}
|
||||
|
||||
public function testToString()
|
||||
{
|
||||
$sut = $this->getEntity();
|
||||
$sut = new InvoiceTemplate();
|
||||
|
||||
$this->assertNull($sut->__toString());
|
||||
$this->assertIsFluent($sut->setName('a template name'));
|
||||
$this->assertEquals('a template name', $sut->__toString());
|
||||
$this->assertEquals('a template name', (string) $sut);
|
||||
self::assertInstanceOf(InvoiceTemplate::class, $sut->setName('a template name'));
|
||||
self::assertEquals('a template name', $sut->__toString());
|
||||
self::assertEquals('a template name', (string) $sut);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class CsvRendererTest extends TestCase
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('timesheet.html.twig')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('foo.html.twig')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('company.docx')));
|
||||
$this->assertTrue($sut->supports($this->getInvoiceDocument('export.csv')));
|
||||
$this->assertTrue($sut->supports($this->getInvoiceDocument('export.csv', true)));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('spreadsheet.xlsx')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('open-spreadsheet.ods')));
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class CsvRendererTest extends TestCase
|
||||
{
|
||||
/** @var CsvRenderer $sut */
|
||||
$sut = $this->getAbstractRenderer(CsvRenderer::class);
|
||||
$document = $this->getInvoiceDocument('export.csv');
|
||||
$document = $this->getInvoiceDocument('export.csv', true);
|
||||
/** @var BinaryFileResponse $response */
|
||||
$response = $sut->render($document, $model);
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ class DebugRendererTest extends TestCase
|
||||
'template.title',
|
||||
'template.payment_terms',
|
||||
'template.due_days',
|
||||
'template.vat_id',
|
||||
'template.contact',
|
||||
'template.payment_details',
|
||||
'query.begin',
|
||||
'query.day',
|
||||
'query.end',
|
||||
@@ -84,6 +87,7 @@ class DebugRendererTest extends TestCase
|
||||
'customer.name',
|
||||
'customer.contact',
|
||||
'customer.company',
|
||||
'customer.vat',
|
||||
'customer.country',
|
||||
'customer.number',
|
||||
'customer.homepage',
|
||||
|
||||
@@ -35,7 +35,7 @@ class OdsRendererTest extends TestCase
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('company.docx')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('export.csv')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('spreadsheet.xlsx')));
|
||||
$this->assertTrue($sut->supports($this->getInvoiceDocument('open-spreadsheet.ods')));
|
||||
$this->assertTrue($sut->supports($this->getInvoiceDocument('open-spreadsheet.ods', true)));
|
||||
}
|
||||
|
||||
public function getTestModel()
|
||||
@@ -52,7 +52,7 @@ class OdsRendererTest extends TestCase
|
||||
/** @var OdsRenderer $sut */
|
||||
$sut = $this->getAbstractRenderer(OdsRenderer::class);
|
||||
$model = $this->getInvoiceModel();
|
||||
$document = $this->getInvoiceDocument('open-spreadsheet.ods');
|
||||
$document = $this->getInvoiceDocument('open-spreadsheet.ods', true);
|
||||
/** @var BinaryFileResponse $response */
|
||||
$response = $sut->render($document, $model);
|
||||
|
||||
|
||||
@@ -47,10 +47,16 @@ trait RendererTestTrait
|
||||
* @param string $filename
|
||||
* @return InvoiceDocument
|
||||
*/
|
||||
protected function getInvoiceDocument(string $filename)
|
||||
protected function getInvoiceDocument(string $filename, bool $testOnly = false)
|
||||
{
|
||||
if (!$testOnly) {
|
||||
return new InvoiceDocument(
|
||||
new \SplFileInfo($this->getInvoiceTemplatePath() . $filename)
|
||||
);
|
||||
}
|
||||
|
||||
return new InvoiceDocument(
|
||||
new \SplFileInfo($this->getInvoiceTemplatePath() . $filename)
|
||||
new \SplFileInfo(__DIR__ . '/../templates/' . $filename)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class XlsxRendererTest extends TestCase
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('foo.html.twig')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('company.docx')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('export.csv')));
|
||||
$this->assertTrue($sut->supports($this->getInvoiceDocument('spreadsheet.xlsx')));
|
||||
$this->assertTrue($sut->supports($this->getInvoiceDocument('spreadsheet.xlsx', true)));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('open-spreadsheet.ods')));
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class XlsxRendererTest extends TestCase
|
||||
/** @var XlsxRenderer $sut */
|
||||
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
|
||||
$model = $this->getInvoiceModel();
|
||||
$document = $this->getInvoiceDocument('spreadsheet.xlsx');
|
||||
$document = $this->getInvoiceDocument('spreadsheet.xlsx', true);
|
||||
/** @var BinaryFileResponse $response */
|
||||
$response = $sut->render($document, $model);
|
||||
|
||||
|
||||
@@ -25,10 +25,7 @@ class InvoiceDocumentRepositoryTest extends TestCase
|
||||
protected static $defaultDocuments = [
|
||||
'company.docx',
|
||||
'default.html.twig',
|
||||
'export.csv',
|
||||
'freelancer.html.twig',
|
||||
'open-spreadsheet.ods',
|
||||
'spreadsheet.xlsx',
|
||||
'timesheet.html.twig',
|
||||
];
|
||||
|
||||
|
||||
@@ -551,6 +551,10 @@
|
||||
<source>label.vat</source>
|
||||
<target>Umsatzsteuer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.vat_id">
|
||||
<source>label.vat_id</source>
|
||||
<target>Umsatzsteuer-ID</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.contact">
|
||||
<source>label.contact</source>
|
||||
<target>Kontakt</target>
|
||||
|
||||
@@ -551,6 +551,10 @@
|
||||
<source>label.vat</source>
|
||||
<target>Tax</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.vat_id">
|
||||
<source>label.vat_id</source>
|
||||
<target>VAT-ID</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.contact">
|
||||
<source>label.contact</source>
|
||||
<target>Contact</target>
|
||||
|
||||
Binary file not shown.
1
var/packages/.gitkeep
Normal file
1
var/packages/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
This directory can store as Kimai plugin/bundle folder.
|
||||
Reference in New Issue
Block a user