Version initiale de Gestion Prix
App Flutter de gestion de stock/prix pour tablette Android (100% local) : scan code-barres (OpenFoodFacts), prix au kilo/litre, packs, inventaire, prix d'achat + marge, filtres de tri, étiquettes PDF, design noir & blanc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
import '../models/produit.dart';
|
||||
|
||||
/// Génère une planche A4 d'étiquettes de prix (nom, prix TTC, prix HT, code-barres),
|
||||
/// imprimable sur une imprimante classique (ex : Canon TS4550).
|
||||
class EtiquettePdf {
|
||||
/// Construit le document PDF pour les [produits] sélectionnés.
|
||||
static Future<Uint8List> construire(List<Produit> produits) async {
|
||||
final doc = pw.Document();
|
||||
|
||||
doc.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.all(12),
|
||||
build: (context) {
|
||||
return [
|
||||
pw.Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: produits.map(_etiquette).toList(),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return doc.save();
|
||||
}
|
||||
|
||||
static pw.Widget _etiquette(Produit p) {
|
||||
// 3 colonnes sur A4 (~186mm utiles / 3 ≈ 62mm).
|
||||
const largeur = 185.0; // points (~65mm)
|
||||
const hauteur = 110.0; // points (~39mm)
|
||||
|
||||
return pw.Container(
|
||||
width: largeur,
|
||||
height: hauteur,
|
||||
padding: const pw.EdgeInsets.all(8),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: PdfColors.grey400, width: 0.7),
|
||||
borderRadius: pw.BorderRadius.circular(6),
|
||||
),
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Text(
|
||||
p.titre,
|
||||
maxLines: 2,
|
||||
overflow: pw.TextOverflow.clip,
|
||||
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
alignment: pw.Alignment.center,
|
||||
child: pw.Column(
|
||||
mainAxisSize: pw.MainAxisSize.min,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Text(
|
||||
_prix(p.prix),
|
||||
style: pw.TextStyle(
|
||||
fontSize: 22, fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
if (p.prixMesure != null)
|
||||
pw.Text(
|
||||
_prixMesure(p),
|
||||
style: const pw.TextStyle(
|
||||
fontSize: 9, color: PdfColors.grey700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_codeBarres(p.codeBarres),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static pw.Widget _codeBarres(String? code) {
|
||||
if (code == null || code.isEmpty) {
|
||||
return pw.SizedBox(height: 22);
|
||||
}
|
||||
final estEan13 = code.length == 13 && int.tryParse(code) != null;
|
||||
return pw.BarcodeWidget(
|
||||
barcode: estEan13 ? pw.Barcode.ean13() : pw.Barcode.code128(),
|
||||
data: code,
|
||||
width: 150,
|
||||
height: 26,
|
||||
drawText: true,
|
||||
textStyle: const pw.TextStyle(fontSize: 6),
|
||||
);
|
||||
}
|
||||
|
||||
static String _prix(double v) => '${v.toStringAsFixed(2).replaceAll('.', ',')} EUR';
|
||||
|
||||
static String _prixMesure(Produit p) {
|
||||
final m = p.prixMesure!;
|
||||
final unite = m.suffixe == '/kg' ? 'kg' : 'L';
|
||||
return 'soit ${m.valeur.toStringAsFixed(2).replaceAll('.', ',')} EUR / $unite';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user