2025-05-23 13:50:57 +02:00
|
|
|
from flask import Flask, request, jsonify, send_file
|
|
|
|
|
from database import Database
|
|
|
|
|
import os
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
db = Database()
|
|
|
|
|
|
|
|
|
|
@app.route('/')
|
|
|
|
|
def index():
|
|
|
|
|
return app.send_static_file('generator.html')
|
|
|
|
|
|
|
|
|
|
@app.route('/generator')
|
|
|
|
|
def generator():
|
|
|
|
|
return app.send_static_file('generator.html')
|
|
|
|
|
|
|
|
|
|
@app.route('/dashboard')
|
|
|
|
|
def dashboard():
|
|
|
|
|
return app.send_static_file('dashboard.html')
|
|
|
|
|
|
|
|
|
|
@app.route('/preview')
|
|
|
|
|
def preview():
|
|
|
|
|
return app.send_static_file('preview.html')
|
|
|
|
|
|
2025-05-23 15:42:32 +02:00
|
|
|
@app.route('/accounting')
|
|
|
|
|
def accounting():
|
|
|
|
|
return app.send_static_file('accounting.html')
|
|
|
|
|
|
2025-05-23 13:50:57 +02:00
|
|
|
@app.route('/api/invoices', methods=['GET'])
|
|
|
|
|
def get_invoices():
|
|
|
|
|
filters = {}
|
|
|
|
|
if request.args.get('status'):
|
|
|
|
|
filters['status'] = request.args.get('status')
|
|
|
|
|
if request.args.get('date_from'):
|
|
|
|
|
filters['date_from'] = request.args.get('date_from')
|
|
|
|
|
if request.args.get('date_to'):
|
|
|
|
|
filters['date_to'] = request.args.get('date_to')
|
|
|
|
|
|
|
|
|
|
invoices = db.get_invoices(filters)
|
|
|
|
|
statistics = db.get_statistics()
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
'invoices': invoices,
|
|
|
|
|
'statistics': statistics
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/invoices/<int:invoice_id>', methods=['GET'])
|
|
|
|
|
def get_invoice(invoice_id):
|
|
|
|
|
invoice = db.get_invoice(invoice_id)
|
|
|
|
|
if invoice:
|
|
|
|
|
return jsonify(invoice)
|
|
|
|
|
return jsonify({'error': 'Facture non trouvée'}), 404
|
|
|
|
|
|
|
|
|
|
@app.route('/api/invoices/<int:invoice_id>/status', methods=['PUT'])
|
|
|
|
|
def update_invoice_status(invoice_id):
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
if 'status' not in data:
|
|
|
|
|
return jsonify({'error': 'Statut manquant'}), 400
|
|
|
|
|
|
|
|
|
|
db.update_invoice_status(invoice_id, data['status'])
|
|
|
|
|
return jsonify({'message': 'Statut mis à jour avec succès'})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/invoices/<int:invoice_id>/download', methods=['GET'])
|
|
|
|
|
def download_invoice(invoice_id):
|
|
|
|
|
invoice = db.get_invoice(invoice_id)
|
|
|
|
|
if not invoice:
|
|
|
|
|
return jsonify({'error': 'Facture non trouvée'}), 404
|
|
|
|
|
|
|
|
|
|
# Créer le dossier pour les fichiers générés s'il n'existe pas
|
|
|
|
|
os.makedirs('generated', exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# Créer un fichier temporaire pour la facture
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
temp_file = os.path.join('generated', f'invoice_{invoice_id}_{timestamp}.typ')
|
|
|
|
|
output_pdf = temp_file.replace('.typ', '.pdf')
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Écrire le contenu Typst
|
|
|
|
|
with open(temp_file, 'w') as f:
|
|
|
|
|
f.write(invoice['typst_content'])
|
|
|
|
|
|
|
|
|
|
# Générer le PDF avec Typst
|
|
|
|
|
result = subprocess.run(['typst', 'compile', temp_file, output_pdf],
|
|
|
|
|
capture_output=True, text=True)
|
|
|
|
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
return jsonify({'error': f'Erreur lors de la génération du PDF: {result.stderr}'}), 500
|
|
|
|
|
|
|
|
|
|
# Vérifier que le PDF a été créé
|
|
|
|
|
if not os.path.exists(output_pdf):
|
|
|
|
|
return jsonify({'error': 'Le PDF n\'a pas été généré'}), 500
|
|
|
|
|
|
|
|
|
|
# Envoyer le PDF
|
|
|
|
|
return send_file(
|
|
|
|
|
output_pdf,
|
|
|
|
|
as_attachment=True,
|
|
|
|
|
download_name=f'invoice_{invoice["invoice_number"]}.pdf'
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
# Nettoyer les fichiers temporaires
|
|
|
|
|
if os.path.exists(temp_file):
|
|
|
|
|
os.remove(temp_file)
|
|
|
|
|
if os.path.exists(output_pdf):
|
|
|
|
|
os.remove(output_pdf)
|
|
|
|
|
|
|
|
|
|
@app.route('/api/invoices', methods=['POST'])
|
|
|
|
|
def create_invoice():
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
# Générer automatiquement le numéro de facture
|
|
|
|
|
invoice_number = db.get_next_invoice_number()
|
|
|
|
|
|
|
|
|
|
# Ajouter le client
|
|
|
|
|
client_id = db.add_client(
|
|
|
|
|
name=data['recipient_name'],
|
|
|
|
|
address=data['recipient_address'],
|
|
|
|
|
postal_code=data['recipient_postal_code'],
|
|
|
|
|
town=data['recipient_town'],
|
|
|
|
|
country=data['recipient_country'],
|
|
|
|
|
vat_number=data.get('recipient_vat_number')
|
|
|
|
|
)
|
|
|
|
|
|
2025-05-23 15:42:32 +02:00
|
|
|
# Traiter les lignes de facturation
|
|
|
|
|
invoice_items = data.get('items', [])
|
|
|
|
|
if not invoice_items:
|
|
|
|
|
# Utiliser une ligne par défaut si aucun élément n'est fourni
|
|
|
|
|
invoice_items = [{"description": "Poong rental", "amount": data['amount']}]
|
|
|
|
|
|
|
|
|
|
# Générer les lignes pour le tableau Typst
|
|
|
|
|
invoice_rows = ""
|
|
|
|
|
for item in invoice_items:
|
|
|
|
|
invoice_rows += f" [{item['description']}], [{item['amount']}],\n"
|
|
|
|
|
|
2025-05-23 13:50:57 +02:00
|
|
|
# Créer le contenu du template Typst
|
|
|
|
|
typst_content = f'''#let language = "{data['language']}"
|
|
|
|
|
#let invoice_number = "{invoice_number}"
|
|
|
|
|
#let amount = "{data['amount']}"
|
|
|
|
|
#let currency = "{data['currency']}"
|
|
|
|
|
#let current_date = datetime.today()
|
|
|
|
|
#let recipient_name = [{data['recipient_name']}]
|
|
|
|
|
#let recipient_adress = [{data['recipient_address']}]
|
|
|
|
|
#let recipient_postal_code = [{data['recipient_postal_code']}]
|
|
|
|
|
#let recipient_town = [{data['recipient_town']}]
|
|
|
|
|
#let recipient_country = [{data['recipient_country']}]
|
|
|
|
|
#let recipient_vat_number = {"none" if data.get('recipient_vat_number') is None else f'"{data["recipient_vat_number"]}"'}
|
|
|
|
|
|
|
|
|
|
// Dictionnaire des traductions
|
|
|
|
|
#let translations = (
|
|
|
|
|
français: (
|
|
|
|
|
sender: "ÉMETTEUR",
|
|
|
|
|
recipient: "DESTINATAIRE",
|
|
|
|
|
invoice: "FACTURE",
|
|
|
|
|
invoice_number: "Facture N° :",
|
|
|
|
|
date: "Date :",
|
|
|
|
|
due_date: "Échéance :",
|
|
|
|
|
description: "Description",
|
|
|
|
|
amount: "Montant",
|
|
|
|
|
total: "Total :",
|
|
|
|
|
vat_notice: "Association non soumise à la TVA selon l'Art. 10 LTVA",
|
|
|
|
|
payment_terms: "Cette facture est payable dans les 30 jours suivant sa réception.",
|
|
|
|
|
banking_info: "COORDONNÉES BANCAIRES",
|
|
|
|
|
bank: "Banque :",
|
|
|
|
|
account_holder: "Titulaire :",
|
|
|
|
|
account_notice: "Compte au nom d'un membre de l'association",
|
|
|
|
|
association_notice: "Association à but non lucratif non inscrite au registre du commerce",
|
|
|
|
|
),
|
|
|
|
|
deutsch: (
|
|
|
|
|
sender: "ABSENDER",
|
|
|
|
|
recipient: "EMPFÄNGER",
|
|
|
|
|
invoice: "RECHNUNG",
|
|
|
|
|
invoice_number: "Rechnungs-Nr.:",
|
|
|
|
|
date: "Datum:",
|
|
|
|
|
due_date: "Fälligkeitsdatum:",
|
|
|
|
|
description: "Beschreibung",
|
|
|
|
|
amount: "Betrag",
|
|
|
|
|
total: "Gesamtbetrag:",
|
|
|
|
|
vat_notice: "Verein nicht mehrwertsteuerpflichtig gemäß Art. 10 MWSTG",
|
|
|
|
|
payment_terms: "Diese Rechnung ist innerhalb von 30 Tagen nach Erhalt zu bezahlen.",
|
|
|
|
|
banking_info: "BANKVERBINDUNG",
|
|
|
|
|
bank: "Bank:",
|
|
|
|
|
account_holder: "Kontoinhaber:",
|
|
|
|
|
account_notice: "Konto auf den Namen eines Vereinsmitglieds",
|
|
|
|
|
association_notice: "Gemeinnütziger Verein, nicht im Handelsregister eingetragen",
|
|
|
|
|
),
|
|
|
|
|
english: (
|
|
|
|
|
sender: "FROM",
|
|
|
|
|
recipient: "TO",
|
|
|
|
|
invoice: "INVOICE",
|
|
|
|
|
invoice_number: "Invoice No.:",
|
|
|
|
|
date: "Date:",
|
|
|
|
|
due_date: "Due date:",
|
|
|
|
|
description: "Description",
|
|
|
|
|
amount: "Amount",
|
|
|
|
|
total: "Total:",
|
|
|
|
|
vat_notice: "Association not subject to VAT according to Art. 10 VAT Act",
|
|
|
|
|
payment_terms: "This invoice is payable within 30 days of receipt.",
|
|
|
|
|
banking_info: "BANKING DETAILS",
|
|
|
|
|
bank: "Bank:",
|
|
|
|
|
account_holder: "Account holder:",
|
|
|
|
|
account_notice: "Account in the name of an association member",
|
|
|
|
|
association_notice: "Non-profit association not registered in the commercial register",
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Sélection du dictionnaire en fonction de la langue
|
|
|
|
|
#let t = translations.at(language, default: translations.français)
|
|
|
|
|
|
|
|
|
|
#set page(
|
|
|
|
|
margin: 2cm,
|
|
|
|
|
numbering: none,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
#set text(font: "Arial", size: 11pt)
|
|
|
|
|
|
|
|
|
|
// En-tête de l'association
|
|
|
|
|
#align(center)[
|
|
|
|
|
#text(weight: "bold", size: 14pt)[Cheap Motion Pictures ]
|
|
|
|
|
|
|
|
|
|
#text(style: "italic")[#t.association_notice]
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
#v(0.5cm)
|
|
|
|
|
|
|
|
|
|
#grid(
|
|
|
|
|
columns: (1fr, 1fr),
|
|
|
|
|
row-gutter: 0.5cm,
|
|
|
|
|
|
|
|
|
|
// Informations de l'émetteur
|
|
|
|
|
align(left)[
|
|
|
|
|
#text(weight: "bold", size: 14pt)[#t.sender]
|
|
|
|
|
#v(0.2cm)
|
|
|
|
|
Cheap Motion Pictures
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
rue de l'ale 1
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
1003 Lausanne, Suisse
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
Tél: +41 77 471 11 34
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
Email: contact\@cheapmo.ch
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// Informations du destinataire
|
|
|
|
|
align(right)[
|
|
|
|
|
#text(weight: "bold", size: 14pt)[#t.recipient]
|
|
|
|
|
#v(0.2cm)
|
|
|
|
|
#recipient_name
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
#recipient_adress
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
#recipient_postal_code #recipient_town, #recipient_country
|
|
|
|
|
|
|
|
|
|
#if recipient_vat_number != none [VAT: #recipient_vat_number] else [ ];
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
#v(0.5cm)
|
|
|
|
|
|
|
|
|
|
// Titre et informations de la facture
|
|
|
|
|
#align(center)[
|
|
|
|
|
#text(weight: "bold", size: 18pt)[#t.invoice]
|
|
|
|
|
#v(0.3cm)
|
|
|
|
|
#t.invoice_number 2025-#invoice_number - #t.date #current_date.display("[day]/[month]/[year]")
|
|
|
|
|
#v(0.2cm)
|
|
|
|
|
#t.due_date 30/03/2025
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
#v(0.5cm)
|
|
|
|
|
|
|
|
|
|
// Tableau des prestations
|
|
|
|
|
#table(
|
|
|
|
|
columns: (auto, 1fr),
|
|
|
|
|
inset: 10pt,
|
|
|
|
|
align: (left, right),
|
|
|
|
|
table.header(
|
|
|
|
|
[*#t.description*],
|
|
|
|
|
[*#t.amount (#currency)* ],
|
|
|
|
|
),
|
2025-05-23 15:42:32 +02:00
|
|
|
{invoice_rows} [*#t.total*], [*#amount #currency*],
|
2025-05-23 13:50:57 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
#v(.5cm)
|
|
|
|
|
|
|
|
|
|
// Informations complémentaires
|
|
|
|
|
#align(left)[
|
|
|
|
|
#t.vat_notice
|
|
|
|
|
#v(0.3cm)
|
|
|
|
|
#t.payment_terms
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
#v(0.5cm)
|
|
|
|
|
|
|
|
|
|
// Coordonnées bancaires avec notification spéciale
|
2025-05-23 15:42:32 +02:00
|
|
|
#text(weight: "bold", size: 13pt)[#t.banking_info]
|
|
|
|
|
#v(0.2cm)
|
|
|
|
|
|
|
|
|
|
// Afficher le compte bancaire en fonction de la devise
|
|
|
|
|
#if currency == "EUR" [
|
|
|
|
|
// Compte pour les transactions en euros
|
2025-05-23 13:50:57 +02:00
|
|
|
#t.bank Wise, Rue du Trône 100, 3rd floor, Brussels, 1050, Belgium
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
IBAN: BE22905094540247
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
BIC/SWIFT: TRWIBEB1XXX
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
#t.account_holder Robin Szymczak
|
2025-05-23 15:42:32 +02:00
|
|
|
] else if currency == "CHF" [
|
|
|
|
|
// Compte pour les transactions en francs suisses
|
|
|
|
|
#t.bank PostFinance SA, Mingerstrasse 20, 3030 Bern, Switzerland
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
IBAN: CH56 0900 0000 1527 2120 9
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
BIC/SWIFT: POFICHBEXXX
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
#t.account_holder Robin Szymczak
|
|
|
|
|
] else [
|
|
|
|
|
// Compte par défaut (identique à EUR pour la compatibilité)
|
|
|
|
|
#t.bank Wise, Rue du Trône 100, 3rd floor, Brussels, 1050, Belgium
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
IBAN: BE22905094540247
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
BIC/SWIFT: TRWIBEB1XXX
|
|
|
|
|
#v(0.1cm)
|
|
|
|
|
#t.account_holder Robin Szymczak
|
|
|
|
|
]
|
2025-05-23 13:50:57 +02:00
|
|
|
|
2025-05-23 15:42:32 +02:00
|
|
|
#v(0.2cm)
|
|
|
|
|
#t.account_notice
|
2025-05-23 13:50:57 +02:00
|
|
|
|
|
|
|
|
// Pied de page avec contact
|
|
|
|
|
#align(center)[
|
|
|
|
|
#v(1cm)
|
|
|
|
|
#line(length: 100%, stroke: 0.5pt + gray)
|
|
|
|
|
#v(0.3cm)
|
|
|
|
|
Cheap Motion Pictures - 1 rue de l'ale - 1003 Lausanne - contact\@cheapmo.ch
|
|
|
|
|
]
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
# Ajouter la facture
|
|
|
|
|
invoice_id = db.add_invoice(
|
|
|
|
|
invoice_number=invoice_number,
|
|
|
|
|
client_id=client_id,
|
|
|
|
|
amount=data['amount'],
|
|
|
|
|
currency=data['currency'],
|
|
|
|
|
language=data['language'],
|
|
|
|
|
typst_content=typst_content
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
'message': 'Facture créée avec succès',
|
|
|
|
|
'invoice_id': invoice_id
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2025-05-23 15:42:32 +02:00
|
|
|
app.run(debug=True, port=5001)
|