mirror of
https://gerrit.hackerspace.pl/hscloud
synced 2025-03-21 06:44:53 +00:00
invoicer initial version
Change-Id: Ib20a96c224f5c055874f72f8f9a04a4dc8bbbc24
This commit is contained in:
parent
2b27fa6a37
commit
bdf2defc07
38 changed files with 20472 additions and 0 deletions
109
personal/arsenicum/invoicer/.gitignore
vendored
Normal file
109
personal/arsenicum/invoicer/.gitignore
vendored
Normal file
|
@ -0,0 +1,109 @@
|
|||
out/
|
||||
.terraform
|
||||
# User-specific stuff:
|
||||
.idea
|
||||
*.iml
|
||||
*id_rsa*
|
||||
md5-file
|
||||
# Gradle:
|
||||
.idea/gradle.xml
|
||||
.idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### Intellij Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
|
||||
### Java ###
|
||||
*.class
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
/build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
# End of https://www.gitignore.io/api/java,intellij,gradle
|
||||
|
||||
.gradle/
|
||||
.idea/
|
||||
id-core.iml
|
||||
**/*.iml
|
||||
build/
|
||||
/.settings/
|
||||
/.classpath
|
||||
/.DS_Store
|
||||
/.project
|
||||
classes/
|
||||
/output/
|
||||
dump.rdb
|
||||
/libs
|
||||
*.DS_Store
|
||||
|
||||
bin/
|
||||
.vscode
|
||||
|
||||
application-scratch.yml
|
||||
|
||||
api-doc/*0.yml
|
||||
|
||||
*.generated*
|
||||
|
||||
ignite/
|
||||
|
||||
# Helm Charts
|
||||
/helm/*/charts/
|
||||
/helm/*/requirements.lock
|
||||
/helm/*/Chart.lock
|
13
personal/arsenicum/invoicer/frontend/.gitignore
vendored
Normal file
13
personal/arsenicum/invoicer/frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
# binaries
|
||||
bin/
|
||||
dist/
|
||||
lib/
|
||||
|
||||
# editors
|
||||
*.swp
|
||||
.idea/
|
||||
.vs/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
|
||||
node_modules
|
18912
personal/arsenicum/invoicer/frontend/package-lock.json
generated
Normal file
18912
personal/arsenicum/invoicer/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
53
personal/arsenicum/invoicer/frontend/package.json
Normal file
53
personal/arsenicum/invoicer/frontend/package.json
Normal file
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@testing-library/dom": "^7.21.4",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"sockjs-client": "^1.4.0",
|
||||
"type-fest": "^0.13.1",
|
||||
"webpack-hot-middleware": "^2.25.1",
|
||||
"webpack-plugin-serve": "^1.5.0",
|
||||
"bootstrap": "^5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"reactstrap": "^8.10.0",
|
||||
"web-vitals": "^1.1.1",
|
||||
"@mui/material": "^5.14.10",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"file-saver": "^2.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"proxy": "http://localhost:8080",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
personal/arsenicum/invoicer/frontend/public/favicon.ico
Normal file
BIN
personal/arsenicum/invoicer/frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
29
personal/arsenicum/invoicer/frontend/public/index.html
Normal file
29
personal/arsenicum/invoicer/frontend/public/index.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
|
||||
<title>Invoicer</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
BIN
personal/arsenicum/invoicer/frontend/public/logo192.png
Normal file
BIN
personal/arsenicum/invoicer/frontend/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
personal/arsenicum/invoicer/frontend/public/logo512.png
Normal file
BIN
personal/arsenicum/invoicer/frontend/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
personal/arsenicum/invoicer/frontend/public/manifest.json
Normal file
25
personal/arsenicum/invoicer/frontend/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
personal/arsenicum/invoicer/frontend/public/robots.txt
Normal file
3
personal/arsenicum/invoicer/frontend/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
42
personal/arsenicum/invoicer/frontend/src/App.css
Normal file
42
personal/arsenicum/invoicer/frontend/src/App.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.container, .container-fluid {
|
||||
margin-top: 20px;
|
||||
}
|
34
personal/arsenicum/invoicer/frontend/src/App.js
Normal file
34
personal/arsenicum/invoicer/frontend/src/App.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { Component } from 'react';
|
||||
import Container from '@mui/material/Container';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import './App.css';
|
||||
import Home from './Home';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import ClientList from './ClientList';
|
||||
import InvoiceList from './InvoiceList';
|
||||
import ClientEdit from "./ClientEdit";
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<Box sx={{ my: 4 }}>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
Invoicer App
|
||||
</Typography>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path='/' exact={true} component={Home}/>
|
||||
<Route path='/invoices' exact={true} component={InvoiceList}/>
|
||||
<Route path='/clients' exact={true} component={ClientList}/>
|
||||
<Route path='/clients/:id' component={ClientEdit}/>
|
||||
</Switch>
|
||||
</Router>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
8
personal/arsenicum/invoicer/frontend/src/App.test.js
Normal file
8
personal/arsenicum/invoicer/frontend/src/App.test.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/Clients/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
23
personal/arsenicum/invoicer/frontend/src/AppNavbar.js
Normal file
23
personal/arsenicum/invoicer/frontend/src/AppNavbar.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React, {Component} from 'react';
|
||||
import {Navbar, NavbarBrand} from 'reactstrap';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
export default class AppNavbar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isOpen: false};
|
||||
this.toggle = this.toggle.bind(this);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.setState({
|
||||
isOpen: !this.state.isOpen
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Navbar color="dark" dark expand="md">
|
||||
<NavbarBrand tag={Link} to="/">Home</NavbarBrand>
|
||||
</Navbar>;
|
||||
}
|
||||
}
|
82
personal/arsenicum/invoicer/frontend/src/ClientEdit.js
Normal file
82
personal/arsenicum/invoicer/frontend/src/ClientEdit.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { Button, Container, Form, FormGroup, Input, Label } from 'reactstrap';
|
||||
import AppNavbar from './AppNavbar';
|
||||
|
||||
class ClientEdit extends Component {
|
||||
|
||||
emptyItem = {
|
||||
name: '',
|
||||
email: ''
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
item: this.emptyItem
|
||||
};
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.props.match.params.id !== 'new') {
|
||||
const client = await (await fetch(`/clients/${this.props.match.params.id}`)).json();
|
||||
this.setState({item: client});
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
const target = event.target;
|
||||
const value = target.value;
|
||||
const name = target.name;
|
||||
let item = {...this.state.item};
|
||||
item[name] = value;
|
||||
this.setState({item});
|
||||
}
|
||||
|
||||
async handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
const {item} = this.state;
|
||||
|
||||
await fetch('/clients' + (item.id ? '/' + item.id : ''), {
|
||||
method: (item.id) ? 'PUT' : 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(item),
|
||||
});
|
||||
this.props.history.push('/clients');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {item} = this.state;
|
||||
const title = <h2>{item.id ? 'Edit Client' : 'Add Client'}</h2>;
|
||||
|
||||
return <div>
|
||||
<AppNavbar/>
|
||||
<Container>
|
||||
{title}
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormGroup>
|
||||
<Label for="name">Name</Label>
|
||||
<Input type="text" name="name" id="name" value={item.name || ''}
|
||||
onChange={this.handleChange} autoComplete="name"/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label for="email">Email</Label>
|
||||
<Input type="text" name="email" id="email" value={item.email || ''}
|
||||
onChange={this.handleChange} autoComplete="email"/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Button color="primary" type="submit">Save</Button>{' '}
|
||||
<Button color="secondary" tag={Link} to="/clients">Cancel</Button>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Container>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(ClientEdit);
|
99
personal/arsenicum/invoicer/frontend/src/ClientList.js
Normal file
99
personal/arsenicum/invoicer/frontend/src/ClientList.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
import React, {Component} from 'react';
|
||||
import {Button, ButtonGroup, Container, Table} from 'reactstrap';
|
||||
import AppNavbar from './AppNavbar';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {saveAs} from 'file-saver';
|
||||
|
||||
class ClientList extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {clients: []};
|
||||
this.remove = this.remove.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch('/clients')
|
||||
.then(response => response.json())
|
||||
.then(data => this.setState({clients: data}));
|
||||
}
|
||||
|
||||
async remove(id) {
|
||||
await fetch(`/clients/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(() => {
|
||||
let updatedClients = [...this.state.clients].filter(i => i.id !== id);
|
||||
this.setState({clients: updatedClients});
|
||||
});
|
||||
}
|
||||
|
||||
async generateInvoice(nip) {
|
||||
fetch(`/invoices`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({nip: `${nip}`, monthOfInvoice: 'a1'}),
|
||||
headers: {
|
||||
'Accept': 'application/pdf',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
alert("Error " + response.status);
|
||||
sessionStorage.clear();
|
||||
return;
|
||||
}
|
||||
const filename = response.headers.get("Content-Disposition").split("filename=")[1];
|
||||
response.blob()
|
||||
.then(blob => saveAs(new Blob([blob]), filename));
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {clients} = this.state;
|
||||
|
||||
const clientList = clients.map(client => {
|
||||
return <tr key={client.nip}>
|
||||
<td style={{whiteSpace: 'nowrap'}}>{client.name}</td>
|
||||
<td>{client.nip}</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<Button size="sm" color="primary" tag={Link} to={"/clients/" + client.nip}>Edit</Button>
|
||||
<Button size="sm" color="danger" onClick={() => this.remove(client.nip)}>Delete</Button>
|
||||
<Button size="sm" color="primary" onClick={() => this.generateInvoice(client.nip)}>Generate
|
||||
invoice</Button>
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppNavbar/>
|
||||
<Container fluid>
|
||||
<div className="float-right">
|
||||
<Button color="success" tag={Link} to="/clients/new">Add Client</Button>
|
||||
</div>
|
||||
<h3>Clients</h3>
|
||||
<Table className="mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="30%">Name</th>
|
||||
<th width="30%">NIP</th>
|
||||
<th width="40%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{clientList}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ClientList;
|
21
personal/arsenicum/invoicer/frontend/src/Home.js
Normal file
21
personal/arsenicum/invoicer/frontend/src/Home.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React, { Component } from 'react';
|
||||
import './App.css';
|
||||
import AppNavbar from './AppNavbar';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Container } from 'reactstrap';
|
||||
|
||||
class Home extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<AppNavbar/>
|
||||
<Container fluid>
|
||||
<Button color="link"><Link to="/clients">Clients</Link></Button>
|
||||
<Button color="link"><Link to="/invoices">Invoices</Link></Button>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Home;
|
75
personal/arsenicum/invoicer/frontend/src/InvoiceList.js
Normal file
75
personal/arsenicum/invoicer/frontend/src/InvoiceList.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Button, ButtonGroup, Container, Table } from 'reactstrap';
|
||||
import AppNavbar from './AppNavbar';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
class InvoiceList extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {invoices: []};
|
||||
this.remove = this.remove.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch('/invoices')
|
||||
.then(response => response.json())
|
||||
.then(data => this.setState({invoices: data}));
|
||||
}
|
||||
|
||||
async remove(id) {
|
||||
await fetch(`/invoices/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(() => {
|
||||
let updatedClients = [...this.state.invoices].filter(i => i.id !== id);
|
||||
this.setState({invoices: updatedClients});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {invoices} = this.state;
|
||||
|
||||
const clientList = invoices.map(invoice => {
|
||||
return <tr key={invoice.id}>
|
||||
<td style={{whiteSpace: 'nowrap'}}>{invoice.invoiceTitle}</td>
|
||||
<td>{invoice.id}</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<Button size="sm" color="primary" tag={Link} to={"/invoices/" + invoice.id}>Edit</Button>
|
||||
<Button size="sm" color="danger" onClick={() => this.remove(invoice.id)}>Delete</Button>
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppNavbar/>
|
||||
<Container fluid>
|
||||
<div className="float-right">
|
||||
<Button color="success" tag={Link} to="/invoices/new">Add Invoice</Button>
|
||||
</div>
|
||||
<h3>Clients</h3>
|
||||
<Table className="mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="30%">Id</th>
|
||||
<th width="30%">Invoice Title</th>
|
||||
<th width="40%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{clientList}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InvoiceList;
|
13
personal/arsenicum/invoicer/frontend/src/index.css
Normal file
13
personal/arsenicum/invoicer/frontend/src/index.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
16
personal/arsenicum/invoicer/frontend/src/index.js
Normal file
16
personal/arsenicum/invoicer/frontend/src/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
1
personal/arsenicum/invoicer/frontend/src/logo.svg
Normal file
1
personal/arsenicum/invoicer/frontend/src/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
13
personal/arsenicum/invoicer/frontend/src/reportWebVitals.js
Normal file
13
personal/arsenicum/invoicer/frontend/src/reportWebVitals.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
5
personal/arsenicum/invoicer/frontend/src/setupTests.js
Normal file
5
personal/arsenicum/invoicer/frontend/src/setupTests.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
180
personal/arsenicum/invoicer/pom.xml
Normal file
180
personal/arsenicum/invoicer/pom.xml
Normal file
|
@ -0,0 +1,180 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>invoicer</artifactId>
|
||||
<groupId>pl.hackerspace</groupId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-bom</artifactId>
|
||||
<version>${log4j2.version}</version>
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>2.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.28</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.16.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xhtmlrenderer</groupId>
|
||||
<artifactId>flying-saucer-pdf-openpdf</artifactId>
|
||||
<version>9.1.22</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<executable>maven</executable>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
<phase>process-classes</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${basedir}/target/classes/static</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>frontend/build</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.13.4</version>
|
||||
<configuration>
|
||||
<workingDirectory>frontend</workingDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>install node</id>
|
||||
<goals>
|
||||
<goal>install-node-and-yarn</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<nodeVersion>${node.version}</nodeVersion>
|
||||
<yarnVersion>${yarn.version}</yarnVersion>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>yarn install</id>
|
||||
<goals>
|
||||
<goal>yarn</goal>
|
||||
</goals>
|
||||
<phase>generate-resources</phase>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>yarn test</id>
|
||||
<goals>
|
||||
<goal>yarn</goal>
|
||||
</goals>
|
||||
<phase>test</phase>
|
||||
<configuration>
|
||||
<arguments>test</arguments>
|
||||
<environmentVariables>
|
||||
<CI>true</CI>
|
||||
</environmentVariables>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>yarn build</id>
|
||||
<goals>
|
||||
<goal>yarn</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<arguments>build</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>3.1.3</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<node.version>v14.18.0</node.version>
|
||||
<yarn.version>v1.12.1</yarn.version>
|
||||
<spring-boot.version>3.1.3</spring-boot.version>
|
||||
<javafaker.version>1.0.2</javafaker.version>
|
||||
<log4j2.version>2.17.1</log4j2.version>
|
||||
</properties>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,49 @@
|
|||
package pl.hackerspace;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import pl.hackerspace.domain.Client;
|
||||
import pl.hackerspace.domain.Invoice;
|
||||
import pl.hackerspace.repository.ClientRepository;
|
||||
import pl.hackerspace.repository.InvoiceRepository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class BoostrapInitialData implements CommandLineRunner {
|
||||
|
||||
private final ClientRepository clientRepository;
|
||||
private final InvoiceRepository invoiceRepository;
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
log.info("Saving new clients");
|
||||
Client client = Client.builder()
|
||||
.price(BigDecimal.valueOf(200))
|
||||
.nip("PL5252497215")
|
||||
.name("Arseniy Sorokin")
|
||||
.addressLine1("ul. Bródnowska 3/23")
|
||||
.addressLine2("03-439 Warszawa, Polska")
|
||||
.serviceName("Dostęp do Internetu - Umowa HSWAW/666 - Opłata abonamentowa %invoice_month_string%")
|
||||
.email("arssorokin@gmail.com")
|
||||
.build();
|
||||
clientRepository.save(client);
|
||||
clientRepository.save(Client.builder()
|
||||
.price(BigDecimal.valueOf(100))
|
||||
.nip("PL111")
|
||||
.name("Pope Francis")
|
||||
.addressLine1("St.Peter's square")
|
||||
.addressLine2("Rome")
|
||||
.serviceName("Dostęp do Internetu - Umowa HSWAW/2137 - Opłata abonamentowa %invoice_month_string%")
|
||||
.email("pope@vatican.va")
|
||||
.build());
|
||||
log.info("Saving last invoice");
|
||||
invoiceRepository.save(Invoice.builder().id(21196).invoiceTitle("FV21196").creationDate(LocalDateTime.now())
|
||||
.client(client).pdfContent(new byte[]{}).build());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package pl.hackerspace;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableJpaRepositories
|
||||
public class SpringBootReactApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringBootReactApplication.class, args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package pl.hackerspace.controller;
|
||||
|
||||
import pl.hackerspace.domain.Client;
|
||||
import pl.hackerspace.repository.ClientRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/clients")
|
||||
public class ClientsController {
|
||||
|
||||
private final ClientRepository clientRepository;
|
||||
|
||||
public ClientsController(ClientRepository clientRepository) {
|
||||
this.clientRepository = clientRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Client> getClients() {
|
||||
return clientRepository.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{nip}")
|
||||
public Client getClient(@PathVariable String nip) {
|
||||
return clientRepository.findById(nip).orElseThrow(RuntimeException::new);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Client> createClient(@RequestBody Client client) throws URISyntaxException {
|
||||
Client savedClient = clientRepository.save(client);
|
||||
return ResponseEntity.created(new URI("/clients/" + savedClient.getNip())).body(savedClient);
|
||||
}
|
||||
|
||||
@PutMapping("/{nip}")
|
||||
public ResponseEntity<Client> updateClient(@PathVariable String nip, @RequestBody Client client) {
|
||||
Client currentClient = clientRepository.findById(nip).orElseThrow(RuntimeException::new);
|
||||
currentClient.setName(client.getName());
|
||||
currentClient.setEmail(client.getEmail());
|
||||
currentClient = clientRepository.save(client);
|
||||
|
||||
return ResponseEntity.ok(currentClient);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{nip}")
|
||||
public ResponseEntity<Void> deleteClient(@PathVariable String nip) {
|
||||
clientRepository.deleteById(nip);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package pl.hackerspace.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import pl.hackerspace.domain.Client;
|
||||
import pl.hackerspace.domain.Invoice;
|
||||
import pl.hackerspace.dto.InvoiceGenerationDto;
|
||||
import pl.hackerspace.service.InvoiceService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/invoices")
|
||||
@RequiredArgsConstructor
|
||||
@CrossOrigin(value = {"*"}, exposedHeaders = {"Content-Disposition"})
|
||||
public class InvoicesController {
|
||||
|
||||
private final InvoiceService invoiceService;
|
||||
|
||||
@PostMapping
|
||||
@ResponseBody
|
||||
public ResponseEntity<Resource> generateSingleInvoice(@RequestBody InvoiceGenerationDto request)
|
||||
throws IOException {
|
||||
return invoiceService.generateNewInvoice(request);
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
@ResponseBody
|
||||
public void generateAllSubscriberInvoices(HttpServletResponse response, @RequestParam String monthOfInvoice)
|
||||
throws IOException {
|
||||
invoiceService.generateInvoicesForAllSubscribers(response, monthOfInvoice);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Invoice> getInvoices() {
|
||||
return invoiceService.findAll();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package pl.hackerspace.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "client")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Client {
|
||||
|
||||
@Id
|
||||
private String nip;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String email;
|
||||
|
||||
private String addressLine1;
|
||||
private String addressLine2;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String serviceName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private BigDecimal price;
|
||||
|
||||
@Builder.Default
|
||||
@Column(nullable = false)
|
||||
private BigDecimal amount = BigDecimal.valueOf(1);
|
||||
|
||||
@Builder.Default
|
||||
@Column(nullable = false)
|
||||
private BigDecimal vat = BigDecimal.valueOf(23);
|
||||
|
||||
@Builder.Default
|
||||
@Column(nullable = false)
|
||||
private int paymentOffsetDays = 14;
|
||||
|
||||
@Builder.Default
|
||||
private boolean subscriber = true;
|
||||
|
||||
private boolean prepaid;
|
||||
|
||||
@OneToMany(mappedBy="client")
|
||||
Set<Invoice> invoices;
|
||||
|
||||
public byte[] getInvoiceForSubscriptionMonth(String monthOfInvoice) {
|
||||
return invoices.stream()
|
||||
.filter(i -> monthOfInvoice.equals(i.getMonthOfSubscription()))
|
||||
.map(Invoice::getPdfContent)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package pl.hackerspace.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "invoice")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Invoice {
|
||||
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String invoiceTitle;
|
||||
|
||||
private String monthOfSubscription;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime creationDate;
|
||||
|
||||
@Column(columnDefinition = "VARBINARY(50000)", nullable = false)
|
||||
private byte[] pdfContent;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(nullable = false)
|
||||
private Client client;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package pl.hackerspace.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class CustomInvoiceDataDto {
|
||||
|
||||
private String customServiceName;
|
||||
|
||||
private BigDecimal customPrice;
|
||||
|
||||
private BigDecimal customAmount = BigDecimal.valueOf(1);
|
||||
|
||||
private BigDecimal customVat = BigDecimal.valueOf(23);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package pl.hackerspace.dto;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class InvoiceGenerationDto {
|
||||
|
||||
private String nip;
|
||||
|
||||
private String monthOfInvoice;
|
||||
|
||||
private CustomInvoiceDataDto customInvoiceData;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package pl.hackerspace.repository;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
import pl.hackerspace.domain.Client;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface ClientRepository extends JpaRepository<Client, String> {
|
||||
List<Client> findBySubscriberTrue();
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package pl.hackerspace.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import pl.hackerspace.domain.Client;
|
||||
import pl.hackerspace.domain.Invoice;
|
||||
|
||||
@Repository
|
||||
public interface InvoiceRepository extends JpaRepository<Invoice, String> {
|
||||
|
||||
@Query(value = "SELECT max(id) FROM Invoice")
|
||||
long getMaxId();
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package pl.hackerspace.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import pl.hackerspace.domain.Client;
|
||||
import pl.hackerspace.domain.Invoice;
|
||||
import pl.hackerspace.dto.CustomInvoiceDataDto;
|
||||
import pl.hackerspace.dto.InvoiceGenerationDto;
|
||||
import pl.hackerspace.repository.ClientRepository;
|
||||
import pl.hackerspace.repository.InvoiceRepository;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static pl.hackerspace.service.TemplateService.withTemplate;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InvoiceService {
|
||||
|
||||
private final ClientRepository clientRepository;
|
||||
|
||||
private final InvoiceRepository invoiceRepository;
|
||||
|
||||
@Transactional
|
||||
public ResponseEntity<Resource> generateNewInvoice(InvoiceGenerationDto generationRequest) throws IOException {
|
||||
Client client = clientRepository.findById(generationRequest.getNip()).orElseThrow(() -> new RuntimeException("Not found"));
|
||||
return withTemplate(template -> {
|
||||
byte[] invoice;
|
||||
if (!client.isSubscriber() || generationRequest.getCustomInvoiceData() != null) {
|
||||
invoice = createPdfInvoice(client, template, null, generationRequest.getCustomInvoiceData());
|
||||
} else {
|
||||
invoice = getOrCreateSubscriptionPdfInvoice(template, client, generationRequest.getMonthOfInvoice());
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.headers(getHttpHeaders(client.getName() + " " + generationRequest.getMonthOfInvoice()))
|
||||
.contentLength(-1)
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(new ByteArrayResource(invoice));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
public void generateInvoicesForAllSubscribers(HttpServletResponse response, String monthOfInvoice) throws IOException {
|
||||
setHeaders(response, "application/zip", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE) + ".zip");
|
||||
List<Client> subscribers = clientRepository.findBySubscriberTrue();
|
||||
withTemplate(template -> {
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream())) {
|
||||
for (Client client : subscribers) {
|
||||
try (InputStream inputStream = new ByteArrayInputStream(
|
||||
getOrCreateSubscriptionPdfInvoice(template, client, monthOfInvoice))) {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(getPdfFilename(client.getName() + " "
|
||||
+ monthOfInvoice)));
|
||||
StreamUtils.copy(inputStream, zipOutputStream);
|
||||
zipOutputStream.flush();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] getOrCreateSubscriptionPdfInvoice(String template, Client client, final String monthOfInvoice) {
|
||||
byte[] invoice = client.getInvoiceForSubscriptionMonth(monthOfInvoice);
|
||||
if (invoice == null) {
|
||||
invoice = createPdfInvoice(client, template, monthOfInvoice, null);
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private static void setHeaders(HttpServletResponse response, String contentType, String filename) {
|
||||
response.setContentType(contentType);
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||
.filename(filename, StandardCharsets.UTF_8)
|
||||
.build()
|
||||
.toString());
|
||||
}
|
||||
|
||||
private byte[] createPdfInvoice(Client client, String template, String monthOfSubscription,
|
||||
CustomInvoiceDataDto customInvoiceData) {
|
||||
LocalDateTime creationDate = LocalDateTime.now();
|
||||
Invoice newInvoice = createNewInvoice(client, creationDate, monthOfSubscription);
|
||||
byte[] bytes = TemplateService.convertHtmlToPdf(template, client, creationDate,
|
||||
newInvoice.getInvoiceTitle(), monthOfSubscription, customInvoiceData);
|
||||
newInvoice.setPdfContent(bytes);
|
||||
save(newInvoice);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static HttpHeaders getHttpHeaders(final String filename) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION);
|
||||
headers.add(HttpHeaders.CONTENT_TYPE, "application/octet-stream");
|
||||
headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + getPdfFilename(filename));
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static String getPdfFilename(String filename) {
|
||||
return filename + ".pdf";
|
||||
}
|
||||
|
||||
public Invoice createNewInvoice(Client client, final LocalDateTime creationDate, String monthOfSubscription) {
|
||||
long nextInvoiceId = invoiceRepository.getMaxId() + 1;
|
||||
return Invoice.builder().id(nextInvoiceId)
|
||||
.invoiceTitle("FV" + nextInvoiceId)
|
||||
.creationDate(creationDate)
|
||||
.monthOfSubscription(monthOfSubscription)
|
||||
.client(client)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void save(Invoice newInvoice) {
|
||||
invoiceRepository.save(newInvoice);
|
||||
}
|
||||
|
||||
public List<Invoice> findAll() {
|
||||
return invoiceRepository.findAll();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package pl.hackerspace.service;
|
||||
|
||||
import com.lowagie.text.pdf.BaseFont;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.xhtmlrenderer.layout.SharedContext;
|
||||
import org.xhtmlrenderer.pdf.ITextRenderer;
|
||||
import pl.hackerspace.domain.Client;
|
||||
import pl.hackerspace.dto.CustomInvoiceDataDto;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class TemplateService {
|
||||
|
||||
public static final DecimalFormat MONEY_FORMAT = twoDecimalsFormatter();
|
||||
public static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM");
|
||||
public static final String HTML_TEMPLATE_FILE = "invoiceTemplates/invoice_one_service.html";
|
||||
|
||||
public static <A> A withTemplate(Function<String, A> execute) throws IOException {
|
||||
try (InputStream resourceAsStream = TemplateService.class.getClassLoader().getResourceAsStream(HTML_TEMPLATE_FILE)) {
|
||||
String template = new String(resourceAsStream.readAllBytes());
|
||||
return execute.apply(template);
|
||||
}
|
||||
}
|
||||
|
||||
static String populateTemplate(String unprocessedTemplate, Client client, final String subscriptionMonth,
|
||||
final LocalDateTime invoiceCreationDate, final String invoiceTitle,
|
||||
CustomInvoiceDataDto customInvoiceData) {
|
||||
boolean isCustom = customInvoiceData != null;
|
||||
BigDecimal price = isCustom ? customInvoiceData.getCustomPrice() : client.getPrice();
|
||||
BigDecimal amount = isCustom ? customInvoiceData.getCustomAmount() : client.getAmount();
|
||||
BigDecimal vat = isCustom ? customInvoiceData.getCustomVat() : client.getVat();
|
||||
String serviceName = isCustom ? customInvoiceData.getCustomServiceName() : client.getServiceName();
|
||||
BigDecimal totalNet = amount.multiply(price);
|
||||
String processedHtml = unprocessedTemplate
|
||||
.replace("%client_name%", client.getName())
|
||||
.replace("%invoice_title%", invoiceTitle)
|
||||
.replace("%client_price%", formatAsMoney(price))
|
||||
.replace("%client_addressLine1%", client.getAddressLine1())
|
||||
.replace("%client_addressLine2%", client.getAddressLine2())
|
||||
.replace("%client_service_name%", serviceName)
|
||||
.replace("%client_nip%", Optional.ofNullable(client.getNip()).map(nip -> "NIP: " + nip).orElse(""))
|
||||
.replace("%client_total_net%", formatAsMoney(totalNet))
|
||||
.replace("%client_total_gross%", formatAsMoney(totalNet.add(percentageValue(totalNet, vat))))
|
||||
.replace("%client_amount%", formatAsMoney(amount))
|
||||
.replace("%client_vat%", formatAsMoney(vat))
|
||||
.replace("%client_total_tax%", formatAsMoney(percentageValue(totalNet, vat)))
|
||||
.replace("%invoice_month_string%", subscriptionMonth)
|
||||
.replace("%client_payment_date%", formatAsDate(invoiceCreationDate.plusDays(client.getPaymentOffsetDays())))
|
||||
.replace("%invoice_date%", formatAsDate(invoiceCreationDate));
|
||||
validateAllPlaceholdersSubstituted(processedHtml);
|
||||
return processedHtml;
|
||||
}
|
||||
|
||||
private static String formatAsMoney(BigDecimal totalNet) {
|
||||
return MONEY_FORMAT.format(totalNet);
|
||||
}
|
||||
|
||||
private static String formatAsDate(LocalDateTime invoiceCreationDate) {
|
||||
return invoiceCreationDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
}
|
||||
|
||||
private static void validateAllPlaceholdersSubstituted(String processed) {
|
||||
Set<String> allMatches = new HashSet<>();
|
||||
Matcher m = Pattern.compile("%(\\w*?)%").matcher(processed);
|
||||
while (m.find()) {
|
||||
allMatches.add(m.group());
|
||||
}
|
||||
if (!allMatches.isEmpty()) {
|
||||
throw new IllegalStateException("There are unsubstituted placeholders in the template: " + allMatches);
|
||||
}
|
||||
}
|
||||
|
||||
private static DecimalFormat twoDecimalsFormatter() {
|
||||
DecimalFormat df = new DecimalFormat();
|
||||
df.setMaximumFractionDigits(2);
|
||||
df.setMinimumFractionDigits(2);
|
||||
df.setGroupingUsed(false);
|
||||
return df;
|
||||
}
|
||||
|
||||
public static BigDecimal percentageValue(BigDecimal base, BigDecimal pct) {
|
||||
return base.multiply(pct).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
static byte[] convertHtmlToPdf(String unprocessedTemplate, Client client, final LocalDateTime creationDate,
|
||||
final String invoiceTitle, final String monthOfInvoice, CustomInvoiceDataDto customInvoiceData) {
|
||||
String processedHtml = populateTemplate(unprocessedTemplate, client, monthOfInvoice, creationDate, invoiceTitle,
|
||||
customInvoiceData);
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
ITextRenderer renderer = new ITextRenderer();
|
||||
SharedContext sharedContext = renderer.getSharedContext();
|
||||
sharedContext.setPrint(true);
|
||||
sharedContext.setInteractive(false);
|
||||
renderer.getFontResolver().addFont("fonts/calibri.ttf", BaseFont.IDENTITY_H, true);
|
||||
renderer.setDocumentFromString(htmlToXhtml(processedHtml));
|
||||
renderer.layout();
|
||||
renderer.createPDF(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Exception during pdf conversion", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String htmlToXhtml(String html) {
|
||||
Document document = Jsoup.parse(html);
|
||||
document.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
|
||||
return document.html();
|
||||
}
|
||||
}
|
BIN
personal/arsenicum/invoicer/src/main/resources/fonts/calibri.ttf
Normal file
BIN
personal/arsenicum/invoicer/src/main/resources/fonts/calibri.ttf
Normal file
Binary file not shown.
|
@ -0,0 +1,102 @@
|
|||
<style type="text/css">
|
||||
body {
|
||||
font-family: Calibri, sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Language" content="pl">
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<table style="width: 100%; border-collapse: collapse; height: 148px;" border="0">
|
||||
<tbody>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 50%; height: 18px;">
|
||||
<h4><span style="font-size: 14px;">Sprzedawca (Seller):</span></h4>
|
||||
</td>
|
||||
<td style="width: 64%; text-align: right; height: 18px;">
|
||||
<h4><span style="font-size: 14px;">Nabywca (Bill to)</span></h4>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 50%; height: 18px;"><span style="font-size: 14px;">Stowarzyszenie Warszawski Hackerspace</span>
|
||||
</td>
|
||||
<td style="width: 64%; text-align: right; height: 18px;"><span
|
||||
style="font-size: 14px;">%client_name%</span></td>
|
||||
</tr>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 50%; height: 18px;"><span style="font-size: 12px;">ul. Wolność 2A</span></td>
|
||||
<td style="width: 64%; text-align: right; height: 18px;"><span style="font-size: 12px;">%client_addressLine1%</span></td>
|
||||
</tr>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 50%; height: 18px;"><span style="font-size: 12px;">01-018 Warszawa</span></td>
|
||||
<td style="width: 64%; text-align: right; height: 18px;"><span style="font-size: 12px;">%client_addressLine2%</span></td>
|
||||
</tr>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 50%; height: 18px;"><span style="font-size: 12px;">NIP: 525-25-40-655</span><span
|
||||
style="font-size: 12px;"><br></span></td>
|
||||
<td style="width: 64%; text-align: right; height: 18px;"><span style="font-size: 12px;">%client_nip%</span></td>
|
||||
</tr>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 50%; height: 18px;"><span style="font-size: 12px;">IBAN: PL 64 1950 0001 2006 0006 4889 0005</span><span
|
||||
style="font-size: 12px;"><br></span></td>
|
||||
<td style="width: 64%; height: 18px;"></td>
|
||||
</tr>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 50%; height: 18px;"><span style="font-size: 12px;">SWIFT/BIC: PKOPPLPW</span><span
|
||||
style="font-size: 12px;"><br></span></td>
|
||||
<td style="width: 64%; height: 18px;"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3><span style="font-size: 16px;">Faktura VAT nr / Invoice No.: %invoice_title%</span></h3>
|
||||
<p><span style="font-size: 16px;"><span style="font-size: 12px;">Data wystawienia: %invoice_date%<br>Termin płatności: %client_payment_date%</span></span>
|
||||
</p>
|
||||
<table style="border-collapse: collapse; width: 100%; height: 54px;" border="1">
|
||||
<tbody>
|
||||
<tr style="height: 36px;background-color: #e5e7e9">
|
||||
<td style="width: 2%; height: 36px; text-align: center;"><span style="font-size: 14px;">Lp<br>No</span></td>
|
||||
<td style="width: 40%; height: 36px; text-align: center;"><span style="font-size: 14px;">Nazwa<br>Description</span></td>
|
||||
<td style="width: 2%; height: 36px; text-align: center;"><span style="font-size: 14px;">Ilość<br>Qt</span></td>
|
||||
<td style="width: 14%; height: 36px; text-align: center;"><span style="font-size: 14px;">Cena netto<br>Unit price</span>
|
||||
</td>
|
||||
<td style="width: 14%; height: 36px; text-align: center;"><span style="font-size: 14px;">Stawka VAT, %<br>Tax Rate</span>
|
||||
</td>
|
||||
<td style="width: 14%; height: 36px; text-align: center;"><span
|
||||
style="font-size: 14px;">Wartość netto<br>Total Excl. Tax</span></td>
|
||||
<td style="width: 14%; height: 36px; text-align: center;"><span
|
||||
style="font-size: 14px;">Wartość brutto<br>Total Incl. Tax</span></td>
|
||||
</tr>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 2%; height: 18px; text-align: center;"><span style="font-size: 14px;">1</span></td>
|
||||
<td style="width: 40%; height: 18px;"><span style="font-size: 14px;">%client_service_name%</span></td>
|
||||
<td style="width: 2%; height: 18px; text-align: center;"><span style="font-size: 14px;">%client_amount%</span></td>
|
||||
<td style="width: 14%; height: 18px; text-align: center;"><span style="font-size: 14px;">%client_price%</span></td>
|
||||
<td style="width: 14%; height: 18px; text-align: center;"><span style="font-size: 14px;">%client_vat%</span></td>
|
||||
<td style="width: 14%; height: 18px; text-align: center;"><span style="font-size: 14px;">%client_total_net%</span></td>
|
||||
<td style="width: 14%; height: 18px; text-align: center;"><span style="font-size: 14px;">%client_total_gross%</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<table style="width: 50%; border-collapse: collapse; height: 56px;" border="0">
|
||||
<tbody>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 65%; height: 18px; text-align: right;">Razem netto / Total Excl. Tax</td>
|
||||
<td style="width: 35%; height: 18px; text-align: right;">%client_total_net%</td>
|
||||
</tr>
|
||||
<tr style="height: 18px;">
|
||||
<td style="width: 65%; height: 18px; text-align: right;">VAT / Total tax</td>
|
||||
<td style="width: 35%; height: 18px; text-align: right;">%client_total_tax%</td>
|
||||
</tr>
|
||||
<tr style="height: 20px;">
|
||||
<td style="width: 65%; height: 20px; text-align: right;"><strong>Razem brutto / Total Incl. Tax</strong></td>
|
||||
<td style="width: 35%; height: 20px; text-align: right;"><strong>%client_total_gross%</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue