Skip to content

Instantly share code, notes, and snippets.

@pawelabrams
Last active May 4, 2020 22:04
Show Gist options
  • Save pawelabrams/2440a1b2be1bc924c3f763e1e8950a6b to your computer and use it in GitHub Desktop.
Save pawelabrams/2440a1b2be1bc924c3f763e1e8950a6b to your computer and use it in GitHub Desktop.
Rachunek - narzędzie do generowania polskich rachunków (faktur nie-VAT) z jedną pozycją. Idealne dla freelancerów.

Rachunek

Po co?

Bo skomplikowane apki są równie bez sensu co wypełnianie formatki w Excelu.

Wymagania

  • PHP, chyba jakiś nowszy (odpalałem pierwszy raz na 7.0, chyba od tego czasu nic nie zruszyłem);
  • przeglądarka umiejąca eksportować/drukować do PDF (np. Firefox, Chrome).

Uruchomienie

Uruchom

php -S 127.0.0.1:9999

Przejdź do

http://localhost:9999

Przy pierwszym uruchomieniu zostanie wygenerowany plik config.json. Uzupełnij go według przykładowych danych.

Wypełnij formularz na localhost:9999. Kliknij generuj.

Voilà!

Możesz też edytować styl rachunku przez zmodyfikowanie wygenerowanego pliku invoice.css.

<?php
/*
* Author: Paweł Abramowicz <contact me using https://abramowicz.website>
* Double licensed under WTFPL/Beerware
* Do Whatever The Fun you want, and if you want, buy me a beer :)
*/
setlocale(LC_ALL, 'pl_PL.UTF-8', 'pl_PL', 'pl', 'Polish_Poland.28592');
if (file_exists('config.json')) {
$config = json_decode(file_get_contents('config.json'));
} else {
$config = (object)[
'numbering' => '\R/Y/n/\1',
'me' => (object)[
'name' => 'UZUPEŁNIJ PLIK KONFIGURACYJNY',
'nip' => '0000000000',
'address' => 'ul. Przykładowa xx, xxxxx Przykładowice',
'tel' => '+48 xxxxxxxxx',
'email' => 'xxxxxxxxxxxxxxxxxx',
'bank' => 'xxxxxxxxxxxxxxxxxx',
'iban' => 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
],
'them' => (object)[
'name' => 'PRZYKŁADOWY KONTRAHENT sp. z o. o.',
'nip' => '0000000000',
'address' => 'ul. Innego Przykładu xx, xxxxx Przykładowice',
],
'item' => 'usługi programistyczne',
'pkwiu' => '62.01.11.0',
'salary' => 0,
'clauses' => 'Sprzedawca zwolniony podmiotowo z podatku VAT (podatku od towarów i usług) zgodnie z art. 113 ust. 1',
];
if (!file_exists('config.dist.json')) {
file_put_contents(
'config.dist.json',
json_encode(
$config,
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
)
);
}
}
if (!isset($_POST['hours'])):
?>
<!DOCTYPE html>
<html lang="pl" dir="ltr">
<head>
<meta charset="utf-8">
<title>Wygeneruj fakturę</title>
<style>
body { font-family: sans-serif; }
.warning { padding: 1em; background: #fa0; }
.generate { padding: 1em; text-align: center; }
.item { padding: .5em; }
.item label { display: inline-block; min-width: 13em; text-align: left; }
.item input { border: none; border-bottom: 2px solid #ccc; width: 12em; }
.generate button { border: none; background: #456; color: #fff; font-weight: bold; padding: 1em 2em; width: 12em; }
footer { color: #ccc; text-align: center; }
footer a { color: #888; text-decoration: none; }
</style>
</head>
<body>
<?php if (!file_exists('config.json')): ?>
<div class="warning">
UWAGA! Utworzono nowy plik konfiguracyjny. Skopiuj plik config.dist.json, nazwij go config.json i uzupełnij.
</div>
<?php endif; ?>
<form class="generate" action="" method="post">
<div class="item">
<label for="number">Numer rachunku:</label>
<input type="text" id="number" name="number"
value="<?php echo date($config->numbering); ?>">
</div>
<div class="item">
<label for="hours">Godziny:</label>
<input type="number" id="hours" name="hours" value="160" step="0.001">
</div>
<div class="item">
<label for="date">Data wystawienia:</label>
<input type="date" id="date" name="date"
value="<?php echo date('Y-m-d'); ?>">
</div>
<div class="item">
<label for="weekdays">Dni robocze na zapłatę:</label>
<input type="number" id="weekdays" name="weekdays" value="7">
</div>
<div class="item">
<label for="premiums">Dofinansowania (np. cowork):</label>
<input type="number" id="premiums" name="premiums" value="270">
</div>
<div class="item">
<button>Wygeneruj</button>
</div>
</form>
<!-- This footer does NOT show up on the invoice, don't remove it if you don't have to ;) -->
<footer>Code by <a href="https://abramowicz.website">Abramowicz</a></footer>
<!-- I can't force you, though! -->
</body>
</html>
<?php
else:
$hours = $_POST['hours'];
$amount = $hours * ($config->salary ?? 0) + ($_POST['premiums'] ?? 0);
$today = new \DateTime($_POST['date'] ?? 'today');
$term = clone $today;
$term->modify(sprintf(
'+%d weekdays',
$_POST['weekdays'] ?? 7
));
[$words_amount, $remainder] = wording($amount);
?>
<!DOCTYPE html>
<html lang="pl" dir="ltr">
<head>
<meta charset="utf-8">
<title>Rachunek nr <?php echo $_POST['number'] ?? date($config->numbering); ?></title>
<link rel="stylesheet" href="invoice.css">
</head>
<body>
<h1>Rachunek nr <?php echo $_POST['number'] ?? date($config->numbering); ?></h1>
<h2>Sprzedawca</h2>
<p><?php echo $config->me->name; ?></p>
<p>NIP <?php echo $config->me->nip; ?></p>
<p><?php echo $config->me->address; ?></p>
<p>tel. <?php echo $config->me->tel; ?></p>
<p><?php echo $config->me->email; ?></p>
<p>bank: <?php echo $config->me->bank; ?></p>
<p>nr konta: <?php echo $config->me->iban; ?></p>
<h2>Nabywca</h2>
<p><?php echo $config->them->name; ?></p>
<p>NIP <?php echo $config->them->nip; ?></p>
<p><?php echo $config->them->address; ?></p>
<h2>Termin</h2>
<p>miejsce wystawienia: Wrocław</p>
<p>data sprzedaży: <?php echo strftime("%e %B %Y", $today->getTimestamp()); ?></p>
<p>data wystawienia: <?php echo strftime("%e %B %Y", $today->getTimestamp()); ?></p>
<p>termin płatności: <?php echo strftime("%e %B %Y", $term->getTimestamp()); ?></p>
<h2>Usługi</h2>
<table>
<tr class="header">
<td>Nazwa</td>
<td>PKWiU</td>
<td>Ilość, jedn.</td>
<td>Cena jedn.</td>
<td>Wartość</td>
</tr>
<tr>
<td><?php echo $config->item; ?></td>
<td><?php echo $config->pkwiu; ?></td>
<td>1 szt.</td>
<td><?php echo number_format($amount, 2, ',', ' '); ?></td>
<td><?php echo number_format($amount, 2, ',', ' '); ?></td>
</tr>
<tr class="total">
<td colspan="4">RAZEM</td>
<td><?php echo number_format($amount, 2, ',', ' '); ?></td>
</tr>
</table>
<h2>Do zapłaty</h2>
<p class="total-amount"><?php echo number_format($amount, 2, ',', ' '); ?> PLN</p>
<p class="total-words">Słownie: <?php echo $words_amount; ?> PLN <?php echo round($remainder * 100); ?>/100</p>
<p class="payment-method">Sposób zapłaty: Przelew</p>
<p class="additional-clauses"><?php echo $config->clauses ?? ''; ?></p>
</body>
</html>
<?php
endif; # isset _POST
function noun_form($no, $one, $two, $many) {
return $no === 1 ? $one : (
($no % 100 < 5 || $no % 100 > 21) && in_array($no % 10, [2, 3, 4]) ? $two :
$many
);
}
function wording($amount) {
$nos = [
1 => 'jeden',
2 => 'dwa',
3 => 'trzy',
4 => 'cztery',
5 => 'pięć',
6 => 'sześć',
7 => 'siedem',
8 => 'osiem',
9 => 'dziewięć',
10 => 'dziesięć',
11 => 'jedenaście',
12 => 'dwanaście',
13 => 'trzynaście',
14 => 'czternaście',
15 => 'piętnaście',
16 => 'szesnaście',
17 => 'siedemnaście',
18 => 'osiemnaście',
19 => 'dziewiętnaście',
20 => 'dwadzieścia',
30 => 'trzydzieści',
40 => 'czterdzieści',
50 => 'pięćdziesiąt',
60 => 'sześćdziesiąt',
70 => 'siedemdziesiąt',
80 => 'osiemdziesiąt',
90 => 'dziewięćdziesiąt',
100 => 'sto',
200 => 'dwieście',
300 => 'trzysta',
400 => 'czterysta',
500 => 'pięćset',
600 => 'sześćset',
700 => 'siedemset',
800 => 'osiemset',
900 => 'dziewięćset',
];
$remainder = $amount;
$words_amount = '';
if ($remainder >= 1000000) {
$millions = intdiv($remainder, 1000000);
if ($millions > 1) $words_amount .= wording($millions)[0];
$words_amount .= noun_form($millions, 'milion ', 'miliony ', 'milionów ');
$remainder %= 1000000;
}
if ($remainder >= 1000) {
$thousands = intdiv($remainder, 1000);
if ($thousands > 1) $words_amount .= wording($thousands)[0];
$words_amount .= noun_form($thousands, 'tysiąc ', 'tysiące ', 'tysięcy ');
$remainder %= 1000;
}
for (; $remainder >= 1;) {
$max = array_filter(
array_keys($nos),
function ($no) use ($remainder) {
return $no <= $remainder;
}
);
if (count($max) < 1) {
break;
}
$max = max($max);
$words_amount .= $nos[$max] . ' ';
$remainder -= $max;
}
return [$words_amount, $remainder];
}
if (!file_exists('invoice.css')) {
file_put_contents('invoice.css',
<<<EOCSS
html, body {
font-family: sans-serif;
}
h2 {
font-size: 1em;
padding: .5em 0;
}
table {
width: 100%;
}
table td {
padding: 0;
}
table .header {
font-weight: bold;
}
table .header td {
padding-bottom: .5em;
}
table .total {
font-weight: bold;
}
table .total td {
padding-top: .65em;
}
p {
margin: 0;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance:textfield; /* Firefox */
}
.total-amount {
font-size: 2em;
font-weight: bold;
}
.total-words,
.payment-method,
.additional-clauses {
margin-top: .5em;
}
EOCSS
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment