init commit
This commit is contained in:
commit
86ab51f672
|
@ -0,0 +1 @@
|
|||
node_modules/
|
|
@ -0,0 +1,19 @@
|
|||
const http = require('http');
|
||||
const express = require('express');
|
||||
const latex = require('node-latex')
|
||||
const ejs = require('ejs');
|
||||
const formidable = require('formidable');
|
||||
const config = require('./config');
|
||||
const app = express();
|
||||
const path = require('path');
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, '/templates'));
|
||||
app.use('/public', express.static(path.join(__dirname, 'public')));
|
||||
|
||||
//TODO: secure headers
|
||||
//TODO: ip ratelimits
|
||||
|
||||
app.use('/', require('./controllers/index'));
|
||||
|
||||
app.listen(config.port, () => console.log(`pdfgen is listening on port ${config.port}`))
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
port: 3001,
|
||||
tmpPath: '/tmp/',
|
||||
autodelete_timer: 1000 * 60 * 10
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
const router = require('express').Router();
|
||||
const {ensureJSON} = require('../helpers/middleware');
|
||||
const request = require('request');
|
||||
|
||||
router.get('/cvr/:cvr', (req, res, next) => {
|
||||
// TODO: ratelimits?
|
||||
const cvr_regex = /^\d{8}$/g;
|
||||
const cvr_api = cvr => `https://cvrapi.dk/api?search=${cvr}&country=dk`;
|
||||
|
||||
if (!cvr_regex.test(req.params.cvr)) {
|
||||
res.statusCode = 400;
|
||||
return next({name: 'Server-side validering', message: 'Forkert CVR format.'});
|
||||
}
|
||||
|
||||
const cvr_api_options = {
|
||||
url: cvr_api(req.params.cvr),
|
||||
headers: {
|
||||
'User-Agent': 'nyt projekt ved ik hvad det hedder endnu kontakt på n@nikobojs.com hvis problem eller nysgerrig, fakturagenerering'
|
||||
}
|
||||
};
|
||||
|
||||
request(cvr_api_options, (err, response, body) => {
|
||||
if (err) return next('server fejl');
|
||||
if (response.statusCode != 200){
|
||||
response.statusCode = 500;
|
||||
return next({name: 'cvr api returnerede en fejl', message: ''});
|
||||
}
|
||||
|
||||
let json_body;
|
||||
try {
|
||||
json_body = JSON.parse(body);
|
||||
res.json(json_body);
|
||||
}
|
||||
catch(e) {
|
||||
res.statusCode = '500';
|
||||
return next({name: 'cvrapi.dk returnerede ikke json', message: ''});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,21 @@
|
|||
const router = require('express').Router();
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.redirect('/pdfgen/faktura');
|
||||
})
|
||||
|
||||
router.use('/pdfgen', require('./pdfgen'));
|
||||
router.use('/api', require('./api'));
|
||||
|
||||
router.use((error, req, res, next) => {
|
||||
if(!res.headersSent){
|
||||
if(res.statusCode === 200) res.statusCode = 500;
|
||||
res.json(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.use((req, res) => {
|
||||
res.status(404).json({name: 'not found', message: ''});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,123 @@
|
|||
const router = require('express').Router();
|
||||
const {ensureJSON} = require('../helpers/middleware');
|
||||
const request = require('request');
|
||||
const ejs = require('ejs');
|
||||
const formidable = require('formidable');
|
||||
const config = require('../config');
|
||||
const latex = require('node-latex');
|
||||
const latexescape = require('../helpers/latexescape');
|
||||
const fs = require('fs');
|
||||
|
||||
router.get('/:name', (req, res, next) => {
|
||||
const template_name = req.params['name'].toLowerCase().replace(/[^a-z]+/g, '');
|
||||
const template_conf = require(`../templates/${template_name}/conf.js`);
|
||||
res.render(`${template_name}/formular.ejs`, {template: template_conf});
|
||||
});
|
||||
|
||||
|
||||
router.post('/upload/image', (req, res, next) => {
|
||||
const form = new formidable.IncomingForm();
|
||||
form.maxFields = 0;
|
||||
form.maxFileSize = 2 * 1024 * 1024;
|
||||
form.type = true;
|
||||
|
||||
let got_file = false;
|
||||
let file_id;
|
||||
|
||||
form.parse(req)
|
||||
|
||||
form.on('fileBegin', (name, file) => {
|
||||
if(got_file) return;
|
||||
got_file = true;
|
||||
|
||||
const fileType = file.type.split('/').pop().toLowerCase();
|
||||
if(!['png', 'jpg', 'jpeg'].includes(fileType)){
|
||||
res.statusCode = 400;
|
||||
form.emit('error', new Error('Ugyldig filtype'));
|
||||
}
|
||||
|
||||
const file_id = randomString(24) + '.' + file.name.split('.').reverse()[0];
|
||||
file.name = file_id;
|
||||
file.path = config.tmpPath + file_id;
|
||||
});
|
||||
|
||||
form.on('file', (name, file) => {
|
||||
file_id = file.name;
|
||||
});
|
||||
|
||||
form.on('end', () => {
|
||||
if(!file_id) {
|
||||
res.statusCode = 400;
|
||||
return res.json({error: 'Du mangler at vælge en fil til upload'});
|
||||
}
|
||||
else{
|
||||
autodelete(file_id);
|
||||
return res.json({'fileid': file_id});
|
||||
}
|
||||
});
|
||||
|
||||
form.on('error', (err) => {
|
||||
if(err.message && err.message.includes('maxFileSize exceeded')){
|
||||
res.statusCode = 413;
|
||||
res.json({name: 'Filen fylder for meget', message: ''});
|
||||
req.socket.end();
|
||||
}
|
||||
else{
|
||||
res.statusCode = res.statusCode == 200 ? 500 : res.statusCode;
|
||||
res.json(err && err.message ? {error: err.message} : {});
|
||||
req.socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.post('/generate/:name', ensureJSON, async (req, res, next) => {
|
||||
|
||||
const template_name = req.params['name'].toLowerCase().replace(/[^a-z]+/g, '');
|
||||
if(typeof(req.body.files) === 'object') {
|
||||
Object.keys(req.body.files).map(f => {
|
||||
req.body.files[f] = req.body.files[f].split('.');
|
||||
req.body.files[f][0] = req.body.files[f][0].replace(/[^a-zA-Z0-9]+/g, '');
|
||||
req.body.files[f] = config.tmpPath + req.body.files[f].join('.');
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const data = latexescape(req.body);
|
||||
const conf = require(`../templates/${template_name}/conf.js`);
|
||||
const rawlatex = await ejs.renderFile(`templates/${template_name}/${conf.latex}`, data, {async: true});
|
||||
const pdf = latex(rawlatex);
|
||||
pdf.pipe(res);
|
||||
}
|
||||
catch(e) {
|
||||
return next({
|
||||
name: 'Kunne ikke generere pdf\'en.',
|
||||
message: 'Er du sikker på at du har udfyldt alle felterne korrekt?'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
function randomString(len) {
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
return alphabet[Math.floor(Math.random() * alphabet.length)] + (len > 1 ? randomString(--len) : '');
|
||||
}
|
||||
|
||||
function autodelete(file_id, timeout = config.autodelete_timer){
|
||||
const file_path = config.tmpPath + file_id;
|
||||
console.log('setting filedelete timeout½')
|
||||
setTimeout(() => {
|
||||
try{
|
||||
console.log('trying to unlink...')
|
||||
fs.unlinkSync(file_path);
|
||||
console.log('successfully deleted', file_path);
|
||||
}
|
||||
catch(e){
|
||||
console.log('error deleting file:');
|
||||
console.log(e);
|
||||
// TODO: log errors silently
|
||||
}
|
||||
}, timeout);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
module.exports = obj => {
|
||||
|
||||
const clear_var = v => {
|
||||
if(v == null || v == undefined) return '';
|
||||
if(typeof(v) === 'object') return clear_obj(v);
|
||||
else if(typeof(v) === 'string'){
|
||||
v = v.replace(/(?![a-zA-Z0-9\s&/\-\.,\%\_#])./g, '');
|
||||
v = v.replace(/&/g, '\\&');
|
||||
v = v.replace(/%/g, '\\%');
|
||||
v = v.replace(/_/g, '\\_');
|
||||
v = v.replace(/#/g, '\\#');
|
||||
return v;
|
||||
}
|
||||
else{
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
const clear_obj = o => {
|
||||
Object.keys(o).map(k => o[k] = clear_var(o[k]));
|
||||
return o;
|
||||
};
|
||||
|
||||
const cleared = clear_obj(obj);
|
||||
console.log('map clear var:');
|
||||
console.log(cleared);
|
||||
|
||||
return cleared;
|
||||
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
module.exports.ensureJSON = (req, res, next) => {
|
||||
let data = "";
|
||||
req.on('data', chunk => { data += chunk})
|
||||
req.on('end', () => {
|
||||
req.rawBody = data;
|
||||
try{
|
||||
req.body = JSON.parse(data);
|
||||
next();
|
||||
}
|
||||
catch(e){
|
||||
res.statusCode = 400;
|
||||
console.log(req.body);
|
||||
next(new Error('Unable to parse json'));
|
||||
}
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "pdfgen",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"author": "",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node app.js",
|
||||
"dev": "nodemon -V .",
|
||||
"tmux": "./tmux_dev.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"ejs": "^2.6.1",
|
||||
"express": "^4.17.1",
|
||||
"formidable": "^1.2.1",
|
||||
"node-latex": "^2.6.0",
|
||||
"request": "^2.88.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.19.1"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"public/*",
|
||||
"templates/*"
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,93 @@
|
|||
@font-face {
|
||||
font-family: 'abeezee';
|
||||
src: url('/public/ABeeZee-Regular.ttf');/* Safari, Android, iOS */
|
||||
}
|
||||
|
||||
body{
|
||||
font-family: 'abeezee', sans-serif;
|
||||
}
|
||||
|
||||
input.vali_err {
|
||||
border: 1px solid #f00;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.vmargin{
|
||||
margin: 20px 0px;
|
||||
}
|
||||
|
||||
|
||||
.fakturatable {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 20px;
|
||||
}
|
||||
|
||||
.formular > div > div > div > label {
|
||||
display: block;
|
||||
margin-bottom: -45px;
|
||||
color: #888;
|
||||
font-size: .8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fakturatable label {
|
||||
color: #888;
|
||||
font-size: .8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.formular > div{
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.formular > div > div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.formular > div > div > div {
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.formular{
|
||||
max-width: 800px;
|
||||
margin: 60px auto 100px auto;
|
||||
background-color: #eff0fa;
|
||||
padding: 40px;
|
||||
}
|
||||
input.small{
|
||||
width: 100px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
#seller, #buyer{
|
||||
display: block;
|
||||
color: #888;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
input:disabled{
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.inline{
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
h1{
|
||||
margin: 28px 0px 0px 0px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
footer{
|
||||
max-width: 800px;
|
||||
margin: 0px auto 60px auto;
|
||||
padding: 0px;
|
||||
color: #888;
|
||||
font-size: .8em;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
console.log('hello js.js');
|
||||
|
||||
window.pdfgen = {};
|
||||
|
||||
const $ = str => {
|
||||
const elems = document.querySelectorAll(str);
|
||||
if(!elems) throw new Error(`DOM elem '${str}' not found`);
|
||||
return (elems.length == 1) ? elems[0] : elems;
|
||||
};
|
||||
|
||||
|
||||
function error(err){
|
||||
console.log('error(): ', err);
|
||||
if(err.name && err.message){
|
||||
alert(`${err.name}\n${err.message}`);
|
||||
}
|
||||
else if(typeof(err) === 'string'){
|
||||
alert(err);
|
||||
}
|
||||
else{
|
||||
console.error(err);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function cvrapi(el){
|
||||
const cvr = el.value;
|
||||
let resjson = null;
|
||||
if(!/\d{8}/g.test(cvr)) return error({
|
||||
el: el,
|
||||
name: 'Forkert CVR format',
|
||||
message: ' '
|
||||
});
|
||||
const resobj = await fetch(`/api/cvr/${cvr}`);
|
||||
console.log('resobj', resobj);
|
||||
try{ resjson = await resobj.json(); }
|
||||
catch(e) {
|
||||
console.log('json error:', e);
|
||||
return error({
|
||||
el: el,
|
||||
name: 'Kunne ikke parse respons body til JSON',
|
||||
message: 'Serveren svarede ikke korrekt, skriv gerne en fejlrapport.'
|
||||
});
|
||||
}
|
||||
|
||||
if(!resobj.ok) return error({
|
||||
el: el,
|
||||
name: 'Server error',
|
||||
message: el.error
|
||||
});
|
||||
|
||||
return resjson;
|
||||
}
|
||||
|
||||
|
||||
function populate(){
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const elems = Array.from(document.getElementsByClassName('var'));
|
||||
|
||||
// TODO: add text type using opts from dataset
|
||||
await Promise.all(elems.map(async (el) => {
|
||||
if(el.dataset.is === 'number'){
|
||||
try {
|
||||
const number = parseInt(el.value);
|
||||
window.pdfgen[el.dataset.var] = number;
|
||||
}
|
||||
catch(e) { throw {
|
||||
el: el,
|
||||
name: 'Client-side input validering',
|
||||
message: 'Værdien kunne ikke konverteres til et tal'
|
||||
}; }
|
||||
}
|
||||
else if(el.dataset.is === 'boolean') {
|
||||
if(typeof(el.checked) !== 'boolean') throw {
|
||||
el: el,
|
||||
name: 'Client-side input validering',
|
||||
message: 'Checkboxens værdi er ikke gyldig :S'
|
||||
};
|
||||
window.pdfgen[el.dataset.var] = el.checked
|
||||
}
|
||||
else if(el.dataset.is === 'string'){
|
||||
if(typeof(el.value) !== 'string' || el.value === '') throw{
|
||||
el: el,
|
||||
name: 'Client-side input validering',
|
||||
message: 'Teksten kunne ikke valideres'
|
||||
};
|
||||
window.pdfgen[el.dataset.var] = el.value;
|
||||
}
|
||||
else if(el.dataset.is === 'file') {
|
||||
if(!window.pdfgen.files) window.pdfgen.files = {};
|
||||
|
||||
let resjson;
|
||||
const data = new FormData();
|
||||
|
||||
if(!el.files[0]) throw {
|
||||
el: el,
|
||||
name: 'Client-side input validering',
|
||||
message: 'Du mangler at vælge en fil til upload'
|
||||
};
|
||||
|
||||
data.append('file', el.files[0]);
|
||||
|
||||
const resobj = await fetch('/pdfgen/upload/image', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
|
||||
try{ resjson = await resobj.json(); }
|
||||
catch(e) {
|
||||
throw {
|
||||
el: el,
|
||||
name: 'Kunne ikke parse respons body til JSON',
|
||||
message: 'Serveren svarede ikke korrekt, skriv gerne en fejlrapport.'
|
||||
};
|
||||
}
|
||||
|
||||
if(!resobj.ok) {
|
||||
// TODO: throw or reject?
|
||||
if(!resjson.error) return reject(resjson);
|
||||
return reject({
|
||||
el: el,
|
||||
name: 'Kunne ikke uploade fil',
|
||||
message: resjson.error
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
window.pdfgen.files[el.dataset.var] = resjson.fileid;
|
||||
}
|
||||
})).then(() => resolve()).catch(e => {
|
||||
if(e.el) {
|
||||
e.el.className += ' vali_err';
|
||||
setTimeout(() => e.el.className = e.el.className.replace(/\ vali_err/g, ''), 3800);
|
||||
}
|
||||
return reject(e);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
</div>
|
||||
<footer>
|
||||
Dette projekt er open-source (<a href="https://www.gnu.org/licenses/gpl-3.0.html">GPLv3</a>) og kan frit benyttes til alle formål.<br />
|
||||
Jeg lægger snart et link til git op, indtil videre må du kontakte mig hvis du er interesseret i koden.<br />
|
||||
Hvis du vil støtte projektet med kode eller donationer, så kontakt mig på 'n snabel-a nikobojs.com'.
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="/public/css.css" />
|
||||
<title>frifaktura.dk - simpel faktura generering, uden reklamer eller dataindsamling.</title>
|
||||
<script src="/public/js.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="formular">
|
|
@ -0,0 +1,17 @@
|
|||
module.exports = {
|
||||
/*
|
||||
Køber CVR
|
||||
Sælger CVR
|
||||
Faktura nr
|
||||
Konto nr
|
||||
Reg nr
|
||||
Fakturadato
|
||||
Betalingsdato
|
||||
Logo fileupload
|
||||
Produkter/services [ Beskrivelse, Antal, Pris ]
|
||||
*/
|
||||
template: 'formular.ejs',
|
||||
latex: 'latex.ejs',
|
||||
author: 'nikobojs',
|
||||
name: 'faktura'
|
||||
}
|
|
@ -0,0 +1,328 @@
|
|||
|
||||
<% include ../base/header %>
|
||||
|
||||
<div>
|
||||
<h1>1. Basic info</h1>
|
||||
<div>
|
||||
<div>
|
||||
<label>Sælger CVR:</label>
|
||||
</div>
|
||||
<div></div>
|
||||
|
||||
<div>
|
||||
<input id="seller_cvr" class="small" type="text" data-is="number" />
|
||||
<input id="seller_btn" type="button" value="Hent data fra cvrapi.dk" />
|
||||
</div>
|
||||
<div>
|
||||
<span id="seller"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<label>Køber CVR:</label>
|
||||
</div>
|
||||
<div></div>
|
||||
|
||||
<div>
|
||||
<input id="buyer_cvr" class="small" type="text" data-is="number" />
|
||||
<input id="buyer_btn" type="button" value="Hent data fra cvrapi.dk" />
|
||||
</div>
|
||||
<div>
|
||||
<span id="buyer"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<label>Faktura nummer:</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>Moms?</label>
|
||||
</div>
|
||||
<div>
|
||||
<input class="var small" data-var="invoiceno" data-is="number" value="12312312" />
|
||||
</div>
|
||||
<div>
|
||||
<input class="var" data-var="vat" data-is="boolean" type="checkbox" checked />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<label>Konto nr.:</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>Reg nr.:</label>
|
||||
</div>
|
||||
<div>
|
||||
<input class="var small" data-var="accountno" data-is="number" value="12312312" />
|
||||
</div>
|
||||
<div>
|
||||
<input class="var small" data-var="regno" data-is="number" value="12312312" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Faktueringsdato:</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>Betalingsdato:</label>
|
||||
</div>
|
||||
<div>
|
||||
<input class="var small" data-var="sent_date" data-is="string" value="11/12/2019" />
|
||||
</div>
|
||||
<div>
|
||||
<input class="var small" data-var="pay_date" data-is="string" value="13/12/2019" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>2. Upload logo</h1>
|
||||
<div>
|
||||
<div>
|
||||
<label>Maks 2 megabytes</label>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<input class="var" data-is="file" data-var="logo" type="file" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- work array -->
|
||||
<div>
|
||||
<h1>3. Tilføj produkt/service</h1>
|
||||
<section class="fakturatable">
|
||||
<div><label>Beskrivelse</label></div>
|
||||
<div><label>Antal</label></div>
|
||||
<div><label>Enhedspris</label></div>
|
||||
<div><label>Samlet</label></div>
|
||||
<div><label></label></div>
|
||||
</section>
|
||||
<section id="added_work"></section>
|
||||
<section class="fakturatable vmargin">
|
||||
<div><input id="work_desc" class="small" /></div>
|
||||
<div><input id="work_amou" class="small" /></div>
|
||||
<div><input id="work_pric" class="small" /></div>
|
||||
<div><button onclick="addWorkLine()">Tilføj linje</button></div>
|
||||
<div></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>4. Hent faktura</h1>
|
||||
<div>
|
||||
<div>
|
||||
<input id="submit" type="button" value="Download PDF" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function removeWorkLine(event, index){
|
||||
window.pdfgen.work.splice(index, 1);
|
||||
const target = event.target.parentNode.parentNode;
|
||||
target.parendNode.removeChild(target);
|
||||
}
|
||||
|
||||
function addWorkLine(){
|
||||
// TODO: validation
|
||||
let work = null;
|
||||
const appendto = $('#added_work');
|
||||
const container = document.createElement('div');
|
||||
container.className = 'fakturatable';
|
||||
|
||||
try{
|
||||
work = {
|
||||
description: $('#work_desc').value,
|
||||
units: parseFloat($('#work_amou').value),
|
||||
unitprice: parseFloat($('#work_pric').value)
|
||||
};
|
||||
}
|
||||
catch(e){
|
||||
return error({name: 'Client-side validering', message: 'Ugyldigt bruger input'});
|
||||
}
|
||||
|
||||
if(!window.pdfgen.work) window.pdfgen.work = [work];
|
||||
else window.pdfgen.work.push(work);
|
||||
|
||||
console.log('added work', work, 'to window.pdfgen', window.pdfgen);
|
||||
|
||||
const description = document.createElement('div');
|
||||
const units = document.createElement('div');
|
||||
const unitprice = document.createElement('div');
|
||||
const sum = document.createElement('div');
|
||||
const remove = document.createElement('button');
|
||||
remove.addEventListener('click', e => removeWorkLine(e, work.length - 1));
|
||||
|
||||
description.innerText = work.description;
|
||||
units.innerText = work.units;
|
||||
unitprice.innerText = work.unitprice;
|
||||
sum.innerText = parseInt(work.units) * parseInt(work.unitprice);
|
||||
|
||||
|
||||
|
||||
container.appendChild(description);
|
||||
container.appendChild(units);
|
||||
container.appendChild(unitprice);
|
||||
container.appendChild(sum);
|
||||
appendto.appendChild(container);
|
||||
}
|
||||
|
||||
/*window.pdfgen = {
|
||||
"sellerasdasd": {
|
||||
"name": "Nikobojs Web & Software",
|
||||
"vat": 38273965,
|
||||
"address": "Polensgade 19, 3. tv.",
|
||||
"zipcode": "2300",
|
||||
"city": "København S",
|
||||
"phone": null,
|
||||
"email": "nikolaj@fabriciusbjerre.dk",
|
||||
"addressco": "Nikolaj Fabricius-Bjerre"
|
||||
},
|
||||
"buyer": {
|
||||
"name": "Nikobojs Web & Software",
|
||||
"vat": 38273965,
|
||||
"address": "Polensgade 19, 3. tv.",
|
||||
"city": "København S",
|
||||
"zipcode": "2300"
|
||||
},
|
||||
"invoice_no": "2019-01-01T00:00:00.000Z"
|
||||
};*/
|
||||
|
||||
|
||||
|
||||
$('#seller_btn').addEventListener('click', async (event) => {
|
||||
$('#seller_cvr').disabled = true;
|
||||
const data = await cvrapi($('#seller_cvr'));
|
||||
$('#seller_cvr').disabled = false;
|
||||
if(typeof(data) !== 'object') return;
|
||||
window.pdfgen.seller = {
|
||||
'name': data.name,
|
||||
'vat': data.vat,
|
||||
'address': data.address,
|
||||
'city': data.city,
|
||||
'zipcode': data.zipcode,
|
||||
'phone': data.phone,
|
||||
'email': data.email,
|
||||
'addressco': data.addressco
|
||||
};
|
||||
|
||||
$('#seller').innerHTML = `
|
||||
${data.name}<br />
|
||||
${data.address}, ${data.zipcode} ${data.city}<br />
|
||||
CVR ${data.vat}<br />
|
||||
`;
|
||||
});
|
||||
|
||||
$('#buyer_btn').addEventListener('click', async (event) => {
|
||||
$('#buyer_cvr').disabled = true;
|
||||
const data = await cvrapi($('#buyer_cvr'));
|
||||
$('#buyer_cvr').disabled = false;
|
||||
if(typeof(data) !== 'object') return;
|
||||
window.pdfgen.buyer = {
|
||||
'name': data.name,
|
||||
'vat': data.vat,
|
||||
'city': data.city,
|
||||
'address': data.address,
|
||||
'zipcode': data.zipcode
|
||||
};
|
||||
|
||||
$('#buyer').innerHTML = `
|
||||
${data.name}<br />
|
||||
${data.address}, ${data.zipcode} ${data.city}<br />
|
||||
CVR ${data.vat}<br />
|
||||
`;
|
||||
});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', e => {
|
||||
// TODO: move to js.js? make formular.ejs simple!!!
|
||||
$('#submit').addEventListener('click', async (event) => {
|
||||
let response;
|
||||
|
||||
try{
|
||||
await populate();
|
||||
}
|
||||
catch(e){
|
||||
return error(e);
|
||||
}
|
||||
|
||||
const url = `generate/<%= locals.template['name'] %>`;
|
||||
try{
|
||||
response = await fetch(url, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
referrer: 'no-referrer',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(window.pdfgen)
|
||||
});
|
||||
|
||||
}
|
||||
catch(e){
|
||||
return error(e);
|
||||
}
|
||||
|
||||
console.log('response is', response);
|
||||
|
||||
if(!response.ok){
|
||||
try{
|
||||
const err = await response.json();
|
||||
return error(err);
|
||||
}
|
||||
catch(e){
|
||||
return error({
|
||||
name: 'Kunne ikke parse respons body til JSON',
|
||||
message: 'Serveren svarede ikke korrekt, skriv gerne en fejlrapport.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const objurl = window.URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = objurl;
|
||||
a.download = 'faktura.pdf'; // TODO
|
||||
$('body').appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
var hello = {
|
||||
'buyer': {
|
||||
'name': {type: 'string', required: true},
|
||||
'vat': {type: 'string', required: true},
|
||||
'address': {type: 'string', required: true},
|
||||
'zipcode': {type: 'string', required: true},
|
||||
'phone': {type: 'string', required: false},
|
||||
'email': {type: 'string', required: false},
|
||||
'addressco': {type: 'string', required: false},
|
||||
},
|
||||
'seller': {
|
||||
'name': {type: 'string', required: true},
|
||||
'vat': {type: 'string', required: true},
|
||||
'address': {type: 'string', required: true},
|
||||
'zipcode': {type: 'string', required: true},
|
||||
},
|
||||
'invoiceno': {type: 'number', required: true},
|
||||
'pay_date': {type: 'date', required: true},
|
||||
'sent_date': {type: 'date', required: true},
|
||||
'accountno': {type: 'number', required: true},
|
||||
'regno': {type: 'number', required: true},
|
||||
'moms': {type: 'boolean', required: true},
|
||||
'work': [{
|
||||
'description': {type: 'string', required: true},
|
||||
'units': {type: 'number', required: true},
|
||||
'unitprice': {type: 'float', required: true}
|
||||
}],
|
||||
'files': {
|
||||
'logo': ['png', 'jpg', 'jpeg']
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<% include ../base/footer %>
|
|
@ -0,0 +1,75 @@
|
|||
<%
|
||||
function formatPrice(n){
|
||||
n = n.toFixed(2);
|
||||
n = n.replace('.', ',');
|
||||
return n;
|
||||
}
|
||||
%>
|
||||
\documentclass[a4paper]{article}
|
||||
|
||||
\usepackage{fancyhdr, graphicx, tabularx, changepage, multicol, array}
|
||||
\usepackage[top=2.0cm, left=2.0cm, right=2.0cm, bottom=2.0cm, headheight=120.5pt, includeheadfoot]{geometry}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{makecell}
|
||||
\usepackage{booktabs}
|
||||
\renewcommand{\familydefault}{\sfdefault}
|
||||
|
||||
\title{Faktura}
|
||||
|
||||
\pagestyle{fancy}
|
||||
|
||||
\headsep=1.4cm
|
||||
\renewcommand{\headrulewidth}{0pt}
|
||||
\rhead{
|
||||
\includegraphics[width=3.5cm]{<%= files.logo %>} \\\bigskip
|
||||
<%- seller["name"] %> \\
|
||||
<%= locals.seller['addressco'] ? ('c/o ' + seller["addressco"] + ' \\\\') : '\\\\' %>
|
||||
CVR <%= seller["vat"] %> \\\smallskip
|
||||
<%- seller["address"] %> \\
|
||||
<%- seller["zipcode"] %> <%- seller["city"] %> \\\smallskip
|
||||
<%= locals.seller['phone'] ? seller["phone"] + ' \\\\' : '%' %>
|
||||
<%= locals.seller['email'] ? seller["email"] + ' \\\\' : '\\\\' %>
|
||||
}
|
||||
\fancyfoot{}
|
||||
|
||||
\newcolumntype{R}{>{\raggedleft\arraybackslash}p{3.2cm}}
|
||||
\renewcommand{\arraystretch}{1.2}
|
||||
\begin{document}
|
||||
|
||||
\vspace{4.5cm}
|
||||
|
||||
\begin{adjustwidth}{0.1cm}{0pt}
|
||||
\begin{Huge}
|
||||
\textbf{\textsf{FAKTURA}} \\
|
||||
\end{Huge}
|
||||
\end{adjustwidth}
|
||||
|
||||
\vspace{1cm}
|
||||
|
||||
\begin{center}
|
||||
\begin{tabularx}{\textwidth}{ @{}X p{2.0cm} p{2.5cm} R@{}}
|
||||
<%- buyer["name"] %> & & \textbf{Fakturanummer:} & <%= invoiceno %> \\
|
||||
<%- buyer["address"] %>, <%- buyer["zipcode"] %> <%- buyer["city"] %> & & \textbf{Fakturadato:} & <%= sent_date %> \\
|
||||
CVR <%= buyer["vat"] %> & & \textbf{Betalingsdato:} & <%= pay_date %> \\
|
||||
& & \textbf{Konto nr.:} & <%= accountno %> \\
|
||||
& & \textbf{Reg nr.:} & <%= regno %> \\ \\ \\ \\
|
||||
|
||||
\textbf{\textsf{Beskrivelse}} & \textbf{\textsf{Antal}} & \textbf{\textsf{Pris}} & \textbf{\textsf{Sum}} \\
|
||||
\hline \\
|
||||
<% for(line of work){ %>
|
||||
<%= line.description %> & <%= formatPrice(line.units) %> & DKK <%= formatPrice(line.unitprice) %> & DKK <%= formatPrice(line.units*line.unitprice) %> \\
|
||||
<% } %>
|
||||
& & & \\
|
||||
\hline \\
|
||||
<% sum = work.reduce((sum, l) => sum += l.units*l.unitprice, 0) %>
|
||||
<% if(vat) { %>
|
||||
& & \textbf{Subtotal:} & DKK <%= formatPrice(sum) %>\\
|
||||
& & \textbf{Moms $($25\%$)$:} & DKK <%= formatPrice(sum/4) %> \\ \\
|
||||
& & \textbf{Total:} & \underline{DKK <%= formatPrice(sum + sum/4) %>}\\
|
||||
<% } else { %>
|
||||
& & \textbf{Total:} & \underline{DKK <%= formatPrice(sum) %>}\\
|
||||
<% } %>
|
||||
\end{tabularx}
|
||||
\end{center}
|
||||
|
||||
\end{document}
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
tmux new-session \; \
|
||||
send-keys "export PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '; clear" C-m \; \
|
||||
send-keys "nodemon ." C-m \; \
|
||||
split-window -v \; \
|
||||
send-keys "export PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '; clear" C-m \; \
|
||||
split-window -h \; \
|
||||
send-keys "export PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '; clear" C-m \; \
|
||||
send-keys 'htop' C-m \; \
|
||||
select-pane -t 1 \;
|
Loading…
Reference in New Issue