initial commit
This commit is contained in:
commit
76c96f2348
8 changed files with 1587 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
vendor
|
||||
private
|
||||
46
README.md
Normal file
46
README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Billator
|
||||
|
||||
A quick and dirty way to create bills in HTML/PDF format from a list of task with duration in days.
|
||||
|
||||
Written in PHP.
|
||||
|
||||
May be adapted in the futur to allow for more use cases.
|
||||
|
||||
Long term goal is to include this code into a bigger project that manage all the life of a freelancer
|
||||
|
||||
## Requirements
|
||||
|
||||
- >= PHP 8.0 (not tested with older versions but may work)
|
||||
- Wkhtmltopdf https://wkhtmltopdf.org/index.html
|
||||
|
||||
## Installation
|
||||
|
||||
- Run `composer install`
|
||||
|
||||
## Usage
|
||||
|
||||
- Copy `bill.example.yaml` and create your own Bill using the structure
|
||||
|
||||
- If you want you can put your taks in a text file with this format:
|
||||
|
||||
```
|
||||
// file tasks.txt
|
||||
Foo bar n°1 // task name
|
||||
0.8 + 2 + 5 // task duration
|
||||
|
||||
Foo bar n° 2
|
||||
3
|
||||
|
||||
==== // new category
|
||||
Foo bar n° 3
|
||||
4
|
||||
```
|
||||
|
||||
Then generate the categories in a yaml format with the script `generate_categories.php`
|
||||
|
||||
- Generate the HTML using the script `generate_html.php`
|
||||
|
||||
- Then finally generate the PDF with this cmd :
|
||||
|
||||
`wkhtmltopdf bill.html bill.pdf`
|
||||
|
||||
21
composer.json
Normal file
21
composer.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "mbess/bill_generator",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mbess\\BillGenerator\\": "src/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matthieu Bessat",
|
||||
"email": "spamfree@matthieubessat.fr"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"symfony/yaml": "^6.0",
|
||||
"dompdf/dompdf": "^1.2",
|
||||
"symfony/expression-language": "^6.0",
|
||||
"twig/twig": "^3.0",
|
||||
"twig/intl-extra": "^3.3"
|
||||
}
|
||||
}
|
||||
1234
composer.lock
generated
Normal file
1234
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
51
generate_categories.php
Normal file
51
generate_categories.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
$r = file_get_contents('./tasks.txt');
|
||||
$lines = explode("\n", $r);
|
||||
|
||||
$expr = new ExpressionLanguage();
|
||||
|
||||
$days = [];
|
||||
$categories = [];
|
||||
$items = [];
|
||||
$currentItem = [];
|
||||
for ($i = 0; $i < count($lines); $i++) {
|
||||
$line = $lines[$i];
|
||||
if (
|
||||
empty($line) ||
|
||||
str_starts_with($line, '#') ||
|
||||
str_starts_with($line,'//')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($line, '==')) {
|
||||
$categories[] = ['name' => 'X', 'tasks' => $items];
|
||||
$items = [];
|
||||
$currentItem = [];
|
||||
continue;
|
||||
}
|
||||
$currentItem[] = $lines[$i];
|
||||
if (count($currentItem) == 2) {
|
||||
$days = $expr->evaluate($currentItem[1]);
|
||||
$total += $days;
|
||||
$items[] = [
|
||||
'name' => $currentItem[0],
|
||||
'days' => $days
|
||||
];
|
||||
$currentItem = [];
|
||||
}
|
||||
}
|
||||
$categories[] = ['name' => 'X', 'tasks' => $items];
|
||||
|
||||
var_dump(count($categories));
|
||||
echo $total . "\n";
|
||||
|
||||
$yaml = Yaml::dump(['categories' => $categories], 5);
|
||||
file_put_contents('./categories.yaml', $yaml);
|
||||
|
||||
41
generate_html.php
Normal file
41
generate_html.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
use Twig\Extra\Intl\IntlExtension;
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
$data = Yaml::parseFile('./bill.yaml');
|
||||
|
||||
$totalDays = 0;
|
||||
foreach ($data['categories'] as $i => $category) {
|
||||
$sub = 0;
|
||||
foreach ($category['tasks'] as $task) {
|
||||
$sub += $task['days'];
|
||||
}
|
||||
$totalDays += $sub;
|
||||
$data['categories'][$i]['total'] = $sub;
|
||||
}
|
||||
|
||||
$total = ceil($totalDays * $data['daily_rate']);
|
||||
$alreadyPayed = false;
|
||||
$data['summary'] = [
|
||||
'days_total' => $totalDays,
|
||||
'total' => $total,
|
||||
'already_payed' => $alreadyPayed ? ceil($alreadyPayed) : false,
|
||||
'remaining' => ceil($total - ($alreadyPayed ? ceil($alreadyPayed) : 0))
|
||||
];
|
||||
|
||||
$loader = new \Twig\Loader\FilesystemLoader('./templates');
|
||||
$twig = new \Twig\Environment($loader, [
|
||||
'cache' => false,
|
||||
]);
|
||||
|
||||
$twig->addExtension(new IntlExtension());
|
||||
|
||||
$template = $twig->load('bill.html.twig');
|
||||
$html = $template->render($data);
|
||||
|
||||
echo $html;
|
||||
81
styles/main.css
Normal file
81
styles/main.css
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
body {
|
||||
background: white;
|
||||
}
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
}
|
||||
.category {
|
||||
border: 1px solid black;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.row {
|
||||
padding: 3px 5px;
|
||||
border-top: 1px dashed rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.category .row:first-of-type {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.sub-total {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.sub-total > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
background: rgba(149, 165, 166, 0.3);
|
||||
padding: 0;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.summary-content .row td {
|
||||
padding: 5px;
|
||||
}
|
||||
.summary-content .row:first-of-type {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.summary-content tr td:first-of-type {
|
||||
padding-right: 20px;
|
||||
}
|
||||
.summary-content tr td:nth-of-type(2) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.header-column {
|
||||
list-style-type: none;
|
||||
padding-left: 0em;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 60px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.banking {
|
||||
display: flex;
|
||||
}
|
||||
.banking div {
|
||||
margin-right: 4em;
|
||||
}
|
||||
111
templates/bill.html.twig
Normal file
111
templates/bill.html.twig
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Facture</title>
|
||||
<style>
|
||||
{% include('../styles/main.css') %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Facture n°{{ id }}</h2>
|
||||
<div class="header">
|
||||
<ul class="header-column provider">
|
||||
<li>Au nom et pour le compte de :</li>
|
||||
<li><b>{{ provider.name }}</b></li>
|
||||
{% for line in provider.address %}
|
||||
<li>{{ line }}</li>
|
||||
{% endfor %}
|
||||
<li><a href="telto:{{ provider.mobile }}">{{ provider.mobile_formatted }}</a></li>
|
||||
<li><a href="mailto:{{ provider.email }}">{{ provider.email }}</a></li>
|
||||
<li>SIRET : {{ provider.siret }}
|
||||
<li>TVA intracom : {{ provider.tax_id }}</li>
|
||||
<li>Code NAF : {{ provider.naf }}</li>
|
||||
</ul>
|
||||
<ul class="header-column customer">
|
||||
<li>Adressé à :</li>
|
||||
<li><b>{{ customer.name }}</b></li>
|
||||
{% for line in customer.address %}
|
||||
<li>{{ line }}</li>
|
||||
{% endfor %}
|
||||
<li>SIRET : {{ customer.siret }}
|
||||
<li>TVA intracom : {{ customer.tax_id }}</li>
|
||||
|
||||
<li>Contact : {{ customer.contact.name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
Le {{ date | format_datetime('full', 'none', locale='fr') }}, <br>
|
||||
</p>
|
||||
<p>
|
||||
<b>Objet : {{ subject }}</b>
|
||||
</p>
|
||||
<br>
|
||||
{% for category in categories %}
|
||||
<div class="category">
|
||||
<div class="row category-name">
|
||||
<div class="col">
|
||||
Tâche
|
||||
</div>
|
||||
<div class="col">
|
||||
Durée (en jours)
|
||||
</div>
|
||||
</div>
|
||||
<div class="row category-name">
|
||||
{{ category.name }}
|
||||
</div>
|
||||
{% for task in category.tasks %}
|
||||
<div class="row task">
|
||||
<div class="col task-name">{{ task.name }}</div>
|
||||
<div class="col task-duration">{{ task.days|format_number(locale='fr') }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="row sub-total">
|
||||
<div>
|
||||
<div>Sous-total</div>
|
||||
<div class="sub-total-amount">{{ category.total|format_number(locale='fr') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="summary">
|
||||
<table class="summary-content">
|
||||
<tr class="row">
|
||||
<td>Total en jours</td>
|
||||
<td>{{ summary.days_total|format_number(locale='fr') }} j</td>
|
||||
</tr>
|
||||
<tr class="row">
|
||||
<td>Taux journalier en euros</td>
|
||||
<td>{{ daily_rate }} €</td>
|
||||
</tr>
|
||||
<tr class="row">
|
||||
<td>TVA (0 %)</td>
|
||||
<td>0 €</td>
|
||||
</tr>
|
||||
<tr class="row">
|
||||
<td>Total TTC</td>
|
||||
<td>{{ summary.total|format_number(locale='fr') }} €</td>
|
||||
</tr>
|
||||
{% if summary.already_payed %}
|
||||
<tr class="row">
|
||||
<td>Déjà payé</td>
|
||||
<td>{{ summary.already_payed|format_number(locale='fr') }} €</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="row">
|
||||
<td>Solde</td>
|
||||
<td><b>{{ summary.remaining|format_number(locale='fr') }} €</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div>Coordonnées bancaires :</div>
|
||||
<div class="banking">
|
||||
<div>IBAN : FR76 1142 5009 0004 2337 9133 213</div> <div>BIC/SWIFT : CEPAFRPP</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue