fix final1
This commit is contained in:
parent
cc37039155
commit
437f973963
|
|
@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS recipe_ingredients (
|
|||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
recipe_id INT NOT NULL,
|
||||
product_name VARCHAR(100) NOT NULL,
|
||||
quantity_needed INT NOT NULL,
|
||||
quantity_needed FLOAT NOT NULL,
|
||||
unit VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
|
||||
|
|
@ -34,9 +34,9 @@ CREATE TABLE IF NOT EXISTS products (
|
|||
product_name VARCHAR(100) NOT NULL UNIQUE,
|
||||
category VARCHAR(50) NOT NULL,
|
||||
price INT NOT NULL,
|
||||
current_stock INT NOT NULL DEFAULT 0,
|
||||
current_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||
unit VARCHAR(20) NOT NULL,
|
||||
min_stock INT DEFAULT 0,
|
||||
min_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_product_name (product_name),
|
||||
KEY idx_category (category)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ class Product {
|
|||
final String name;
|
||||
final String category;
|
||||
final int price;
|
||||
final int stock;
|
||||
final double stock;
|
||||
final double minStock;
|
||||
final String unit;
|
||||
final String status; // 'tersedia', 'sedang', 'kritis'
|
||||
|
||||
|
|
@ -13,17 +14,27 @@ class Product {
|
|||
required this.category,
|
||||
required this.price,
|
||||
required this.stock,
|
||||
this.minStock = 0,
|
||||
this.unit = 'kg',
|
||||
this.status = 'tersedia',
|
||||
});
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) {
|
||||
final stockRaw = json['stock'] ?? json['current_stock'] ?? 0;
|
||||
final minStockRaw = json['min_stock'] ?? json['stok_minimum'] ?? 0;
|
||||
return Product(
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String,
|
||||
category: json['category'] as String,
|
||||
price: json['price'] as int,
|
||||
stock: json['stock'] as int,
|
||||
stock:
|
||||
stockRaw is num
|
||||
? stockRaw.toDouble()
|
||||
: double.tryParse(stockRaw.toString()) ?? 0,
|
||||
minStock:
|
||||
minStockRaw is num
|
||||
? minStockRaw.toDouble()
|
||||
: double.tryParse(minStockRaw.toString()) ?? 0,
|
||||
unit: (json['unit'] as String?) ?? 'kg',
|
||||
status: json['status'] as String? ?? 'tersedia',
|
||||
);
|
||||
|
|
@ -36,6 +47,7 @@ class Product {
|
|||
'category': category,
|
||||
'price': price,
|
||||
'stock': stock,
|
||||
'min_stock': minStock,
|
||||
'unit': unit,
|
||||
'status': status,
|
||||
};
|
||||
|
|
@ -46,7 +58,8 @@ class Product {
|
|||
String? name,
|
||||
String? category,
|
||||
int? price,
|
||||
int? stock,
|
||||
double? stock,
|
||||
double? minStock,
|
||||
String? unit,
|
||||
String? status,
|
||||
}) {
|
||||
|
|
@ -56,6 +69,7 @@ class Product {
|
|||
category: category ?? this.category,
|
||||
price: price ?? this.price,
|
||||
stock: stock ?? this.stock,
|
||||
minStock: minStock ?? this.minStock,
|
||||
unit: unit ?? this.unit,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -109,7 +109,11 @@ class DashboardController extends ChangeNotifier {
|
|||
if (item is! Map) continue;
|
||||
|
||||
final stockValue = StockStatusUtils.parseStock(item['current_stock']);
|
||||
final statusKey = StockStatusUtils.statusFromStock(stockValue);
|
||||
final minStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||
final statusKey = StockStatusUtils.statusFromStock(
|
||||
stockValue,
|
||||
minStock: minStock,
|
||||
);
|
||||
if (statusKey != StockStatusUtils.statusKritis) continue;
|
||||
|
||||
final category = item['category']?.toString().toLowerCase() ?? '';
|
||||
|
|
|
|||
|
|
@ -88,13 +88,12 @@ class PredictionController extends ChangeNotifier {
|
|||
(product['name'] ?? product['product_name'] ?? '').toString();
|
||||
if (name.isEmpty) continue;
|
||||
|
||||
final id = product['id'] ?? 0;
|
||||
final id = _toInt(product['id']);
|
||||
final unit = (product['unit'] ?? '').toString();
|
||||
final stockRaw = product['current_stock'] ?? 0;
|
||||
final stock = stockRaw is num ? stockRaw.toDouble() : 0.0;
|
||||
final stock = _toDouble(product['current_stock'] ?? product['stock']);
|
||||
|
||||
currentStock[name] = stock;
|
||||
if (id is int) {
|
||||
if (id != null) {
|
||||
productIds[name] = id;
|
||||
}
|
||||
if (unit.isNotEmpty) {
|
||||
|
|
@ -162,7 +161,7 @@ class PredictionController extends ChangeNotifier {
|
|||
|
||||
ingredients.forEach((productName, details) {
|
||||
if (!isIngredientSelected(productName)) return;
|
||||
final quantity = (details['quantity'] as num).toDouble();
|
||||
final quantity = _toDouble(details['quantity']);
|
||||
required[productName] = quantity * productionQuantity;
|
||||
});
|
||||
|
||||
|
|
@ -294,6 +293,17 @@ class PredictionController extends ChangeNotifier {
|
|||
return normalized;
|
||||
}
|
||||
|
||||
double _toDouble(dynamic value) {
|
||||
if (value is num) return value.toDouble();
|
||||
return double.tryParse(value?.toString() ?? '') ?? 0;
|
||||
}
|
||||
|
||||
int? _toInt(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
|
||||
double _convertToStockUnit({
|
||||
required String ingredient,
|
||||
required double amount,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ class ProductListController extends ChangeNotifier {
|
|||
|
||||
products =
|
||||
fetchedProducts.map((p) {
|
||||
final stock = p['current_stock'] ?? 0;
|
||||
final stock = StockStatusUtils.parseStock(p['current_stock']);
|
||||
final minStock = StockStatusUtils.parseStock(p['min_stock']);
|
||||
final category = p['category'] ?? '';
|
||||
final unit =
|
||||
p['unit'] ??
|
||||
|
|
@ -30,8 +31,12 @@ class ProductListController extends ChangeNotifier {
|
|||
category: category,
|
||||
price: p['price'] ?? 0,
|
||||
stock: stock,
|
||||
minStock: minStock,
|
||||
unit: unit,
|
||||
status: StockStatusUtils.statusFromStock(stock),
|
||||
status: StockStatusUtils.statusFromStock(
|
||||
stock,
|
||||
minStock: minStock,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
} finally {
|
||||
|
|
@ -74,8 +79,16 @@ class ProductListController extends ChangeNotifier {
|
|||
}
|
||||
|
||||
int get maxStock {
|
||||
return maxStockValue.ceil().clamp(1, double.infinity).toInt();
|
||||
}
|
||||
|
||||
double get maxStockValue {
|
||||
if (products.isEmpty) return 1;
|
||||
return products.fold<int>(0, (max, p) => p.stock > max ? p.stock : max);
|
||||
final maxValue = products.fold<double>(
|
||||
0,
|
||||
(max, p) => p.stock > max ? p.stock : max,
|
||||
);
|
||||
return maxValue <= 0 ? 1 : maxValue;
|
||||
}
|
||||
|
||||
Color getStatusColor(String status) {
|
||||
|
|
@ -107,6 +120,14 @@ class ProductListController extends ChangeNotifier {
|
|||
|
||||
String formatPrice(int price) => 'Rp ${(price ~/ 1000)}K';
|
||||
|
||||
String formatStock(double value) {
|
||||
if (value % 1 == 0) return value.toInt().toString();
|
||||
return value
|
||||
.toStringAsFixed(3)
|
||||
.replaceAll(RegExp(r'0+$'), '')
|
||||
.replaceAll(RegExp(r'\.$'), '');
|
||||
}
|
||||
|
||||
String getStatusLabel(String status) {
|
||||
return StockStatusUtils.label(status, withIcon: true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
|||
}
|
||||
|
||||
Widget _buildProductCard(Product product) {
|
||||
final maxStock = _controller.maxStock;
|
||||
final maxStock = _controller.maxStockValue;
|
||||
final stockPercentage = (product.stock / maxStock * 100).toInt();
|
||||
|
||||
return InkWell(
|
||||
|
|
@ -488,7 +488,7 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${product.stock} ${product.unit}',
|
||||
'${_controller.formatStock(product.stock)} ${product.unit}',
|
||||
style: AppTextStyles.labelLarge.copyWith(
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
|
|
@ -560,7 +560,7 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
|||
}
|
||||
|
||||
void _showProductDetail(Product product) {
|
||||
final maxStock = _controller.maxStock;
|
||||
final maxStock = _controller.maxStockValue;
|
||||
final stockPercentage =
|
||||
maxStock == 0 ? 0 : (product.stock / maxStock * 100).round();
|
||||
final statusColor = _controller.getStatusColor(product.status);
|
||||
|
|
@ -672,7 +672,14 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
|||
_buildDetailTile(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
label: 'Stok',
|
||||
value: '${product.stock} ${product.unit}',
|
||||
value:
|
||||
'${_controller.formatStock(product.stock)} ${product.unit}',
|
||||
),
|
||||
_buildDetailTile(
|
||||
icon: Icons.low_priority_outlined,
|
||||
label: 'Minimum',
|
||||
value:
|
||||
'${_controller.formatStock(product.minStock)} ${product.unit}',
|
||||
),
|
||||
_buildDetailTile(
|
||||
icon: Icons.straighten_outlined,
|
||||
|
|
|
|||
|
|
@ -176,7 +176,11 @@ class ReportController extends ChangeNotifier {
|
|||
if (item is! Map) continue;
|
||||
|
||||
final stock = StockStatusUtils.parseStock(item['current_stock']);
|
||||
final status = StockStatusUtils.statusFromStock(stock);
|
||||
final minStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||
final status = StockStatusUtils.statusFromStock(
|
||||
stock,
|
||||
minStock: minStock,
|
||||
);
|
||||
if (status != StockStatusUtils.statusKritis) continue;
|
||||
|
||||
final category = item['category']?.toString().toLowerCase() ?? '';
|
||||
|
|
|
|||
|
|
@ -371,11 +371,14 @@ class _ReportScreenState extends State<ReportScreen> {
|
|||
itemBuilder: (context, index) {
|
||||
final item = sortedItems[index];
|
||||
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
||||
final statusKey = StockStatusUtils.statusFromStock(stockValue);
|
||||
final minimumStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||
final statusKey = StockStatusUtils.statusFromStock(
|
||||
stockValue,
|
||||
minStock: minimumStock,
|
||||
);
|
||||
final statusColor = StockStatusUtils.color(statusKey);
|
||||
final statusLabel = StockStatusUtils.label(statusKey);
|
||||
final unit = item['unit']?.toString() ?? 'kg';
|
||||
final minimumStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||
|
||||
return _buildReportItemCard(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
|
|
@ -1468,7 +1471,13 @@ class _ReportScreenState extends State<ReportScreen> {
|
|||
...stockItems.asMap().entries.map((entry) {
|
||||
final item = entry.value;
|
||||
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
||||
final statusKey = StockStatusUtils.statusFromStock(stockValue);
|
||||
final minimumStock = StockStatusUtils.parseStock(
|
||||
item['min_stock'],
|
||||
);
|
||||
final statusKey = StockStatusUtils.statusFromStock(
|
||||
stockValue,
|
||||
minStock: minimumStock,
|
||||
);
|
||||
return [
|
||||
'${entry.key + 1}',
|
||||
item['name']?.toString() ?? '-',
|
||||
|
|
@ -1620,7 +1629,11 @@ class _ReportScreenState extends State<ReportScreen> {
|
|||
for (var index = 0; index < stockItems.length; index++) {
|
||||
final item = stockItems[index];
|
||||
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
||||
final statusKey = StockStatusUtils.statusFromStock(stockValue);
|
||||
final minimumStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||
final statusKey = StockStatusUtils.statusFromStock(
|
||||
stockValue,
|
||||
minStock: minimumStock,
|
||||
);
|
||||
final statusLabel = StockStatusUtils.label(statusKey);
|
||||
buffer.writeln(
|
||||
'${index + 1},${_escapeCsv(item['name'])},'
|
||||
|
|
@ -1709,8 +1722,16 @@ class _ReportScreenState extends State<ReportScreen> {
|
|||
return items.toList()..sort((a, b) {
|
||||
final aStock = StockStatusUtils.parseStock(a['stock']);
|
||||
final bStock = StockStatusUtils.parseStock(b['stock']);
|
||||
final aStatus = StockStatusUtils.statusFromStock(aStock);
|
||||
final bStatus = StockStatusUtils.statusFromStock(bStock);
|
||||
final aMinStock = StockStatusUtils.parseStock(a['min_stock']);
|
||||
final bMinStock = StockStatusUtils.parseStock(b['min_stock']);
|
||||
final aStatus = StockStatusUtils.statusFromStock(
|
||||
aStock,
|
||||
minStock: aMinStock,
|
||||
);
|
||||
final bStatus = StockStatusUtils.statusFromStock(
|
||||
bStock,
|
||||
minStock: bMinStock,
|
||||
);
|
||||
final statusComparison = _statusOrder(
|
||||
aStatus,
|
||||
).compareTo(_statusOrder(bStatus));
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ class TransactionController extends ChangeNotifier {
|
|||
required String category,
|
||||
required int price,
|
||||
required int initialStock,
|
||||
required double minStock,
|
||||
required String unit,
|
||||
required String productType,
|
||||
}) async {
|
||||
|
|
@ -221,6 +222,7 @@ class TransactionController extends ChangeNotifier {
|
|||
category: category,
|
||||
price: price,
|
||||
currentStock: initialStock,
|
||||
minStock: minStock,
|
||||
unit: unit,
|
||||
productType: productType,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
|||
final newProductNameController = TextEditingController();
|
||||
final newPriceController = TextEditingController();
|
||||
final newStockController = TextEditingController();
|
||||
final newMinStockController = TextEditingController();
|
||||
const unitOptionsByType = {
|
||||
'Bahan': ['kg'],
|
||||
'Barang': ['pcs'],
|
||||
|
|
@ -268,6 +269,30 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
|||
fillColor: AppColors.bgLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Stok Minimum',
|
||||
style: AppTextStyles.labelLarge.copyWith(
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: newMinStockController,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
'Batas minimum stok untuk peringatan',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.bgLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
|
|
@ -308,11 +333,16 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
|||
newPriceController.text.trim();
|
||||
final stockText =
|
||||
newStockController.text.trim();
|
||||
final minStockText =
|
||||
newMinStockController.text
|
||||
.trim()
|
||||
.replaceAll(',', '.');
|
||||
|
||||
if (productName.isEmpty ||
|
||||
category.isEmpty ||
|
||||
priceText.isEmpty ||
|
||||
stockText.isEmpty) {
|
||||
stockText.isEmpty ||
|
||||
minStockText.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
|
|
@ -361,12 +391,37 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
|||
return;
|
||||
}
|
||||
|
||||
final minStock =
|
||||
double.tryParse(minStockText) ??
|
||||
-1;
|
||||
if (minStock < 0) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
'Stok minimum tidak boleh negatif',
|
||||
),
|
||||
backgroundColor:
|
||||
AppColors.statusError,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final navigator = Navigator.of(
|
||||
context,
|
||||
);
|
||||
final messenger =
|
||||
ScaffoldMessenger.of(context);
|
||||
|
||||
final result = await _controller
|
||||
.createProduct(
|
||||
productName: productName,
|
||||
category: category,
|
||||
price: priceInt,
|
||||
initialStock: stockInt,
|
||||
minStock: minStock,
|
||||
unit: selectedUnit,
|
||||
productType:
|
||||
selectedProductType,
|
||||
|
|
@ -375,10 +430,8 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
|||
if (!mounted) return;
|
||||
|
||||
if (result['status'] == 'success') {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
navigator.pop();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Produk "$productName" berhasil ditambahkan',
|
||||
|
|
@ -388,9 +441,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
|||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
result['message'] ??
|
||||
|
|
@ -724,7 +775,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
|||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.statusError
|
||||
.withOpacity(0.1),
|
||||
.withValues(alpha: 0.1),
|
||||
borderRadius:
|
||||
BorderRadius.circular(6),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class MLService {
|
|||
// API URL - Change based on environment
|
||||
// Untuk emulator Android: 10.0.2.2
|
||||
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
||||
static const String baseUrl = 'http://192.168.1.16:5000';
|
||||
static const String baseUrl = 'http://192.168.1.2:5000';
|
||||
|
||||
static const int timeoutSeconds = 30;
|
||||
|
||||
|
|
@ -501,6 +501,7 @@ class MLService {
|
|||
required String category,
|
||||
required int price,
|
||||
required int currentStock,
|
||||
double? minStock,
|
||||
String? unit,
|
||||
String? productType,
|
||||
}) async {
|
||||
|
|
@ -510,6 +511,7 @@ class MLService {
|
|||
'category': category,
|
||||
'price': price,
|
||||
'current_stock': currentStock,
|
||||
if (minStock != null) 'min_stock': minStock,
|
||||
if (unit != null) 'unit': unit,
|
||||
if (productType != null) 'product_type': productType,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,8 +16,15 @@ class StockStatusUtils {
|
|||
return double.tryParse(value?.toString() ?? '') ?? 0.0;
|
||||
}
|
||||
|
||||
static String statusFromStock(num stockKg) {
|
||||
static String statusFromStock(num stockKg, {num? minStock}) {
|
||||
final stock = stockKg.toDouble();
|
||||
final minimum = minStock?.toDouble() ?? 0;
|
||||
if (minimum > 0) {
|
||||
if (stock <= minimum) return statusKritis;
|
||||
if (stock <= minimum * 2) return statusSedang;
|
||||
return statusTersedia;
|
||||
}
|
||||
|
||||
if (stock < criticalStockLimitKg) return statusKritis;
|
||||
if (stock < warningStockLimitKg) return statusSedang;
|
||||
return statusTersedia;
|
||||
|
|
|
|||
165
ml_model/app.py
165
ml_model/app.py
|
|
@ -108,6 +108,87 @@ def has_product_unit_column(connection) -> bool:
|
|||
cursor.close()
|
||||
return has_unit
|
||||
|
||||
def ensure_product_stock_precision(connection):
|
||||
if not table_exists(connection, 'products'):
|
||||
return
|
||||
|
||||
stock_col = get_existing_column(connection, 'products', ['current_stock'])
|
||||
if not stock_col:
|
||||
return
|
||||
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("SHOW COLUMNS FROM products LIKE 'current_stock'")
|
||||
column = cursor.fetchone()
|
||||
column_type = (column or {}).get('Type', '').lower()
|
||||
if any(kind in column_type for kind in ['decimal', 'float', 'double']):
|
||||
return
|
||||
|
||||
cursor.execute(
|
||||
"ALTER TABLE products MODIFY current_stock DECIMAL(10,3) NOT NULL DEFAULT 0"
|
||||
)
|
||||
connection.commit()
|
||||
except Error as e:
|
||||
logger.warning(f"Could not update current_stock precision: {e}")
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def ensure_product_min_stock_column(connection):
|
||||
if not table_exists(connection, 'products'):
|
||||
return
|
||||
|
||||
min_col = get_existing_column(connection, 'products', ['min_stock'])
|
||||
if min_col:
|
||||
return
|
||||
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"ALTER TABLE products ADD COLUMN min_stock DECIMAL(10,3) NOT NULL DEFAULT 0"
|
||||
)
|
||||
connection.commit()
|
||||
except Error as e:
|
||||
logger.warning(f"Could not add min_stock column: {e}")
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def backfill_default_min_stock(connection):
|
||||
if not table_exists(connection, 'products'):
|
||||
return
|
||||
|
||||
min_col = get_existing_column(connection, 'products', ['min_stock'])
|
||||
if not min_col:
|
||||
return
|
||||
|
||||
name_col = get_product_name_column(connection)
|
||||
default_minimums = {
|
||||
'Tepung Terigu 1kg': 10,
|
||||
'Telur 1kg': 5,
|
||||
'Gula Pasir 1kg': 8,
|
||||
'Susu Bubuk': 4,
|
||||
'Cokelat Bubuk 250gr': 3,
|
||||
'Mentega 500gr': 5,
|
||||
'Keju Parut 250gr': 2,
|
||||
'Baking Powder': 2,
|
||||
}
|
||||
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
for product_name, min_stock in default_minimums.items():
|
||||
cursor.execute(
|
||||
f"""
|
||||
UPDATE products
|
||||
SET {min_col} = %s
|
||||
WHERE {name_col} = %s AND COALESCE({min_col}, 0) = 0
|
||||
""",
|
||||
(min_stock, product_name)
|
||||
)
|
||||
connection.commit()
|
||||
except Error as e:
|
||||
logger.warning(f"Could not backfill min_stock defaults: {e}")
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def escape_table_name(table_name: str) -> str:
|
||||
return f"`{table_name}`"
|
||||
|
||||
|
|
@ -253,6 +334,11 @@ def fetch_stock_report(connection):
|
|||
if not table_name:
|
||||
return None, 'Tabel bahan atau products tidak ditemukan'
|
||||
|
||||
if table_name == 'products':
|
||||
ensure_product_stock_precision(connection)
|
||||
ensure_product_min_stock_column(connection)
|
||||
backfill_default_min_stock(connection)
|
||||
|
||||
name_col = get_existing_column(connection, table_name, ['nama_bahan', 'product_name', 'name'])
|
||||
stock_col = get_existing_column(connection, table_name, ['stok', 'current_stock', 'stock'])
|
||||
min_col = get_existing_column(connection, table_name, ['stok_minimum', 'min_stock', 'minimum_stock'])
|
||||
|
|
@ -1109,6 +1195,10 @@ def get_products():
|
|||
if not connection:
|
||||
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
||||
|
||||
ensure_product_stock_precision(connection)
|
||||
ensure_product_min_stock_column(connection)
|
||||
backfill_default_min_stock(connection)
|
||||
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM products ORDER BY name")
|
||||
products = cursor.fetchall()
|
||||
|
|
@ -1184,6 +1274,9 @@ def create_product():
|
|||
if not connection:
|
||||
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
||||
|
||||
ensure_product_stock_precision(connection)
|
||||
ensure_product_min_stock_column(connection)
|
||||
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Optional columns compatibility (works for old/new schemas)
|
||||
|
|
@ -1191,9 +1284,12 @@ def create_product():
|
|||
has_unit_column = cursor.fetchone() is not None
|
||||
cursor.execute("SHOW COLUMNS FROM products LIKE 'product_type'")
|
||||
has_product_type_column = cursor.fetchone() is not None
|
||||
cursor.execute("SHOW COLUMNS FROM products LIKE 'min_stock'")
|
||||
has_min_stock_column = cursor.fetchone() is not None
|
||||
|
||||
unit_value = data.get('unit')
|
||||
product_type_value = data.get('product_type')
|
||||
min_stock_value = data.get('min_stock', 0)
|
||||
|
||||
# Check for duplicate product name
|
||||
cursor.execute("SELECT id FROM products WHERE name = %s", (data['name'],))
|
||||
|
|
@ -1205,55 +1301,24 @@ def create_product():
|
|||
'message': f'Product "{data["name"]}" already exists'
|
||||
}), 409
|
||||
|
||||
# Insert new product
|
||||
if has_unit_column and has_product_type_column:
|
||||
cursor.execute("""
|
||||
INSERT INTO products
|
||||
(name, category, price, current_stock, unit, product_type)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
data['name'],
|
||||
data['category'],
|
||||
data['price'],
|
||||
data['current_stock'],
|
||||
unit_value,
|
||||
product_type_value
|
||||
))
|
||||
elif has_unit_column:
|
||||
cursor.execute("""
|
||||
INSERT INTO products
|
||||
(name, category, price, current_stock, unit)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""", (
|
||||
data['name'],
|
||||
data['category'],
|
||||
data['price'],
|
||||
data['current_stock'],
|
||||
unit_value
|
||||
))
|
||||
elif has_product_type_column:
|
||||
cursor.execute("""
|
||||
INSERT INTO products
|
||||
(name, category, price, current_stock, product_type)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""", (
|
||||
data['name'],
|
||||
data['category'],
|
||||
data['price'],
|
||||
data['current_stock'],
|
||||
product_type_value
|
||||
))
|
||||
else:
|
||||
cursor.execute("""
|
||||
INSERT INTO products
|
||||
(name, category, price, current_stock)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (
|
||||
data['name'],
|
||||
data['category'],
|
||||
data['price'],
|
||||
data['current_stock']
|
||||
))
|
||||
columns = ['name', 'category', 'price', 'current_stock']
|
||||
values = [data['name'], data['category'], data['price'], data['current_stock']]
|
||||
if has_unit_column:
|
||||
columns.append('unit')
|
||||
values.append(unit_value)
|
||||
if has_product_type_column:
|
||||
columns.append('product_type')
|
||||
values.append(product_type_value)
|
||||
if has_min_stock_column:
|
||||
columns.append('min_stock')
|
||||
values.append(min_stock_value)
|
||||
|
||||
column_sql = ', '.join(columns)
|
||||
placeholders = ', '.join(['%s'] * len(columns))
|
||||
cursor.execute(
|
||||
f"INSERT INTO products ({column_sql}) VALUES ({placeholders})",
|
||||
tuple(values)
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
product_id = cursor.lastrowid
|
||||
|
|
@ -1305,6 +1370,8 @@ def consume_stock():
|
|||
if not connection:
|
||||
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
||||
|
||||
ensure_product_stock_precision(connection)
|
||||
|
||||
ensure_stock_usage_tables(connection)
|
||||
|
||||
name_column = get_product_name_column(connection)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ CREATE TABLE IF NOT EXISTS products (
|
|||
product_type VARCHAR(20) DEFAULT 'Bahan',
|
||||
unit VARCHAR(20) DEFAULT 'kg',
|
||||
price INT NOT NULL,
|
||||
current_stock INT NOT NULL DEFAULT 0,
|
||||
current_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||
min_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
|
@ -100,15 +101,15 @@ CREATE TABLE IF NOT EXISTS recipe_ingredients (
|
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Insert Initial Products (8 items)
|
||||
INSERT INTO products (name, category, product_type, unit, price, current_stock) VALUES
|
||||
('Tepung Terigu 1kg', 'Tepung', 'Bahan', 'kg', 15000, 50),
|
||||
('Telur 1kg', 'Telur', 'Bahan', 'kg', 25000, 30),
|
||||
('Gula Pasir 1kg', 'Gula', 'Bahan', 'kg', 12000, 40),
|
||||
('Susu Bubuk', 'Susu', 'Bahan', 'kg', 20000, 20),
|
||||
('Cokelat Bubuk 250gr', 'Cokelat', 'Bahan', 'kg', 18000, 15),
|
||||
('Mentega 500gr', 'Mentega', 'Bahan', 'kg', 22000, 25),
|
||||
('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10),
|
||||
('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35);
|
||||
INSERT INTO products (name, category, product_type, unit, price, current_stock, min_stock) VALUES
|
||||
('Tepung Terigu 1kg', 'Tepung', 'Bahan', 'kg', 15000, 50, 10),
|
||||
('Telur 1kg', 'Telur', 'Bahan', 'kg', 25000, 30, 5),
|
||||
('Gula Pasir 1kg', 'Gula', 'Bahan', 'kg', 12000, 40, 8),
|
||||
('Susu Bubuk', 'Susu', 'Bahan', 'kg', 20000, 20, 4),
|
||||
('Cokelat Bubuk 250gr', 'Cokelat', 'Bahan', 'kg', 18000, 15, 3),
|
||||
('Mentega 500gr', 'Mentega', 'Bahan', 'kg', 22000, 25, 5),
|
||||
('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10, 2),
|
||||
('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35, 2);
|
||||
|
||||
-- Insert Default Login Account
|
||||
INSERT INTO login (name, email, username, password) VALUES
|
||||
|
|
|
|||
Loading…
Reference in New Issue