Files
Mathew 4d936d2ee3 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>
2026-07-17 21:28:33 +02:00

186 lines
5.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Unités de vente supportées (poids et volume).
class Unites {
static const g = 'g';
static const kg = 'kg';
static const ml = 'ml';
static const cl = 'cl';
static const l = 'L';
/// Ancienne valeur (produits d'avant) : vendu à l'unité. Plus proposée.
static const piece = 'piece';
static const tout = [g, kg, ml, cl, l];
/// Vrai si le code est une unité de mesure valide (donc pas 'piece'/null).
static bool estValide(String? code) => tout.contains(code);
static String libelle(String? code) => switch (code) {
g => 'g',
kg => 'kg',
ml => 'ml',
cl => 'cl',
l => 'L',
_ => '',
};
}
/// Un produit du stock.
class Produit {
final String id;
final String? codeBarres;
final String nom;
final String? imageUrl;
/// Prix de vente (au client).
final double prix;
/// Prix d'achat (coût). 0 = non renseigné.
final double prixAchat;
/// Contenance d'UN contenant (ex : 330 pour 330 ml, 400 pour 400 g).
final double? quantite;
/// Unité de la quantité : voir [Unites].
final String? unite;
/// Nombre de contenants pour un pack (ex : 6 pour un pack de 6 bières).
/// Null ou 1 = produit à l'unité (pas un pack).
final int? nbContenants;
/// Horodatage de création (ms) — sert au tri par ordre d'ajout.
final int creeLe;
/// Quantité en stock (inventaire).
final int stock;
const Produit({
required this.id,
this.codeBarres,
required this.nom,
this.imageUrl,
required this.prix,
this.prixAchat = 0,
this.quantite,
this.unite,
this.nbContenants,
this.creeLe = 0,
this.stock = 0,
});
bool get estPack => (nbContenants ?? 1) > 1;
/// Gain en euros (vente achat). Pertinent seulement si un prix d'achat est saisi.
double get gain => prix - prixAchat;
/// Coefficient multiplicateur (prix de vente ÷ prix d'achat), ex : 1,5.
/// Null si le prix d'achat n'est pas renseigné. Sert au tri.
double? get coefficient => prixAchat > 0 ? prix / prixAchat : null;
/// Marge affichée : le coefficient (1,5) suivi d'un « % », ex : « 1,5 % », « 2 % ».
/// Vide si le prix d'achat n'est pas renseigné.
String get margeLibelle {
final c = coefficient;
if (c == null) return '';
var s = c.toStringAsFixed(2).replaceAll('.', ',');
if (s.contains(',')) {
s = s.replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r',$'), '');
}
return '$s %';
}
Produit copyWith({
String? id,
String? codeBarres,
String? nom,
String? imageUrl,
double? prix,
double? prixAchat,
double? quantite,
String? unite,
int? nbContenants,
int? creeLe,
int? stock,
}) {
return Produit(
id: id ?? this.id,
codeBarres: codeBarres ?? this.codeBarres,
nom: nom ?? this.nom,
imageUrl: imageUrl ?? this.imageUrl,
prix: prix ?? this.prix,
prixAchat: prixAchat ?? this.prixAchat,
quantite: quantite ?? this.quantite,
unite: unite ?? this.unite,
nbContenants: nbContenants ?? this.nbContenants,
creeLe: creeLe ?? this.creeLe,
stock: stock ?? this.stock,
);
}
/// Prix à l'unité de mesure (€/kg ou €/L) — obligation légale pour les
/// produits vendus au poids/volume. Null pour les produits à la pièce.
({double valeur, String suffixe})? get prixMesure {
final q = quantite;
if (q == null || q <= 0) return null;
// Pour un pack, on calcule sur le volume/poids TOTAL (contenance × nombre).
final total = q * (nbContenants ?? 1);
return switch (unite) {
Unites.g => (valeur: prix / (total / 1000), suffixe: '/kg'),
Unites.kg => (valeur: prix / total, suffixe: '/kg'),
Unites.ml => (valeur: prix / (total / 1000), suffixe: '/L'),
Unites.cl => (valeur: prix / (total / 100), suffixe: '/L'),
Unites.l => (valeur: prix / total, suffixe: '/L'),
_ => null,
};
}
/// Libellé compact de la quantité, ex : « 330ml », « 1kg », « 75cl »,
/// ou « 6x33cl » pour un pack. Vide si absent.
String get quantiteLibelle {
final q = quantite;
if (q == null || q <= 0 || !Unites.estValide(unite)) return '';
final n = q == q.roundToDouble()
? q.toInt().toString()
: q.toString().replaceAll('.', ',');
final base = '$n${Unites.libelle(unite)}';
return estPack ? '${nbContenants}X$base' : base;
}
/// Titre affiché (liste + étiquette) : la quantité est intégrée devant le nom,
/// ex : « 1kg Farine », « 75cl Coca-Cola ». Sinon juste le nom.
String get titre {
final ql = quantiteLibelle;
return ql.isEmpty ? nom : '$ql $nom';
}
factory Produit.fromMap(Map<String, dynamic> map) {
return Produit(
id: map['id'].toString(),
codeBarres: map['code_barres'] as String?,
nom: (map['nom'] as String?) ?? '',
imageUrl: map['image_url'] as String?,
prix: (map['prix'] as num?)?.toDouble() ?? 0,
prixAchat: (map['prix_achat'] as num?)?.toDouble() ?? 0,
quantite: (map['quantite'] as num?)?.toDouble(),
unite: map['unite'] as String?,
nbContenants: (map['nb_contenants'] as num?)?.toInt(),
creeLe: (map['cree_le'] as num?)?.toInt() ?? 0,
stock: (map['stock'] as num?)?.toInt() ?? 0,
);
}
/// Map pour insertion/mise à jour dans la base (sans l'id, généré par SQLite).
Map<String, dynamic> toInsertMap() {
return {
'code_barres': codeBarres,
'nom': nom,
'image_url': imageUrl,
'prix': prix,
'prix_achat': prixAchat,
'quantite': quantite,
'unite': unite,
'nb_contenants': nbContenants,
'stock': stock,
};
}
}