Menambah database produk, resep dan menentukan satua pada produksi
This commit is contained in:
parent
343e540332
commit
ac2bb063bd
|
|
@ -202,7 +202,15 @@ class PredictionController extends ChangeNotifier {
|
|||
String getStockUnit(String ingredient) {
|
||||
final unit = productUnits[ingredient];
|
||||
if (unit != null && unit.isNotEmpty) return unit;
|
||||
return getIngredientUnit(ingredient);
|
||||
return 'kg';
|
||||
}
|
||||
|
||||
double toGram({required double amount, required String unit}) {
|
||||
final normalized = unit.toLowerCase();
|
||||
if (normalized == 'gr') return amount;
|
||||
if (normalized == 'kg') return amount * 1000;
|
||||
if (normalized == 'butir') return amount * eggGramPerButir;
|
||||
return amount;
|
||||
}
|
||||
|
||||
double getRequiredInStockUnit(String ingredient) {
|
||||
|
|
@ -269,23 +277,9 @@ class PredictionController extends ChangeNotifier {
|
|||
return amount;
|
||||
}
|
||||
|
||||
double _applyRoundingForStock(double amountInStockUnit, String stockUnit) {
|
||||
final unit = stockUnit.toLowerCase();
|
||||
if (unit != 'kg' && unit != 'gr') return amountInStockUnit;
|
||||
|
||||
final grams = unit == 'kg' ? amountInStockUnit * 1000 : amountInStockUnit;
|
||||
double _roundRequiredGramToKg(double grams) {
|
||||
if (grams <= 0) return 0;
|
||||
|
||||
double roundedKg;
|
||||
if (grams < 500) {
|
||||
roundedKg = 0.5;
|
||||
} else if (grams <= 1000) {
|
||||
roundedKg = 1.0;
|
||||
} else {
|
||||
roundedKg = (grams / 1000).ceilToDouble();
|
||||
}
|
||||
|
||||
return unit == 'kg' ? roundedKg : roundedKg * 1000;
|
||||
return MLService.gramsToKgRounded(grams);
|
||||
}
|
||||
|
||||
Map<String, double> get roundedUsage {
|
||||
|
|
@ -294,52 +288,55 @@ class PredictionController extends ChangeNotifier {
|
|||
|
||||
required.forEach((ingredient, neededAmount) {
|
||||
if (!isIngredientSelected(ingredient)) return;
|
||||
final stockUnit = getStockUnit(ingredient);
|
||||
final requiredInStockUnit = _convertToStockUnit(
|
||||
ingredient: ingredient,
|
||||
amount: neededAmount,
|
||||
fromUnit: getIngredientUnit(ingredient),
|
||||
);
|
||||
rounded[ingredient] = _applyRoundingForStock(
|
||||
requiredInStockUnit,
|
||||
stockUnit,
|
||||
);
|
||||
final unit = getIngredientUnit(ingredient);
|
||||
final requiredGram = toGram(amount: neededAmount, unit: unit);
|
||||
rounded[ingredient] = _roundRequiredGramToKg(requiredGram);
|
||||
});
|
||||
|
||||
return rounded;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> submitProduction() async {
|
||||
if (selectedRecipe == null || !isCalculated) {
|
||||
return {'status': 'error', 'message': 'Hitung kebutuhan terlebih dahulu'};
|
||||
}
|
||||
|
||||
await refreshStock();
|
||||
if (insufficientStock.isNotEmpty) {
|
||||
if (isSubmitting) {
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Stok masih kurang, silakan update stok dulu',
|
||||
'message': 'Permintaan sedang diproses, mohon tunggu',
|
||||
};
|
||||
}
|
||||
|
||||
final rounded = roundedUsage;
|
||||
if (rounded.isEmpty) {
|
||||
return {'status': 'error', 'message': 'Tidak ada bahan yang dipilih'};
|
||||
}
|
||||
|
||||
isSubmitting = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (selectedRecipe == null || !isCalculated) {
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Hitung kebutuhan terlebih dahulu',
|
||||
};
|
||||
}
|
||||
|
||||
await refreshStock();
|
||||
if (insufficientStock.isNotEmpty) {
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Stok masih kurang, silakan update stok dulu',
|
||||
};
|
||||
}
|
||||
|
||||
final rounded = roundedUsage;
|
||||
if (rounded.isEmpty) {
|
||||
return {'status': 'error', 'message': 'Tidak ada bahan yang dipilih'};
|
||||
}
|
||||
|
||||
final items =
|
||||
rounded.entries.map((entry) {
|
||||
rounded.entries.where((entry) => entry.value > 0).map((entry) {
|
||||
final ingredient = entry.key;
|
||||
final quantity = entry.value;
|
||||
final quantityKg = entry.value;
|
||||
return {
|
||||
'product_id': productIds[ingredient],
|
||||
'product_name': ingredient,
|
||||
'quantity': quantity,
|
||||
'unit': getStockUnit(ingredient),
|
||||
'quantity': quantityKg,
|
||||
'unit': 'kg',
|
||||
};
|
||||
}).toList();
|
||||
|
||||
|
|
@ -359,6 +356,7 @@ class PredictionController extends ChangeNotifier {
|
|||
double.infinity,
|
||||
);
|
||||
}
|
||||
await refreshStock();
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
}
|
||||
|
||||
Future<void> _submitProduction() async {
|
||||
if (_controller.isSubmitting) return;
|
||||
final result = await _controller.submitProduction();
|
||||
if (!mounted) return;
|
||||
|
||||
|
|
@ -606,8 +607,6 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
_controller.getIngredientUnit(
|
||||
ingredient,
|
||||
);
|
||||
final stockUnit = _controller
|
||||
.getStockUnit(ingredient);
|
||||
final quantityPerUnit =
|
||||
(details['quantity'] as num)
|
||||
.toDouble();
|
||||
|
|
@ -617,18 +616,24 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
_controller
|
||||
.productionQuantity
|
||||
: 0.0;
|
||||
final availableAmount =
|
||||
final stockKg =
|
||||
_controller
|
||||
.currentStock[ingredient] ??
|
||||
0.0;
|
||||
final requiredGram =
|
||||
isSelected
|
||||
? _controller.toGram(
|
||||
amount: neededAmount,
|
||||
unit: unit,
|
||||
)
|
||||
: 0.0;
|
||||
final requiredInStockUnit =
|
||||
_controller
|
||||
.getRequiredInStockUnit(
|
||||
ingredient,
|
||||
);
|
||||
final isSufficient =
|
||||
availableAmount >=
|
||||
requiredInStockUnit;
|
||||
stockKg >= requiredInStockUnit;
|
||||
final statusColor =
|
||||
isSelected
|
||||
? (isSufficient
|
||||
|
|
@ -720,7 +725,7 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
),
|
||||
),
|
||||
Text(
|
||||
'${_controller.formatQuantity(neededAmount)} $unit',
|
||||
'${_controller.formatQuantity(requiredGram)} gr',
|
||||
style: const TextStyle(
|
||||
fontSize:
|
||||
12,
|
||||
|
|
@ -750,7 +755,7 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
),
|
||||
),
|
||||
Text(
|
||||
'${_controller.formatQuantity(availableAmount)} $stockUnit',
|
||||
'${_controller.formatQuantity(stockKg)} kg',
|
||||
style: const TextStyle(
|
||||
fontSize:
|
||||
12,
|
||||
|
|
|
|||
|
|
@ -5,10 +5,23 @@ 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.62:5000';
|
||||
static const String baseUrl = 'http://192.168.1.12:5000';
|
||||
|
||||
static const int timeoutSeconds = 30;
|
||||
|
||||
/// Convert grams to kilograms with rounding rules:
|
||||
/// - <= 500g -> 0.5kg
|
||||
/// - 500g < g <= 1000g -> 1kg
|
||||
/// - > 1000g -> round up to nearest 0.5kg
|
||||
static double gramsToKgRounded(num grams) {
|
||||
if (grams <= 0) {
|
||||
throw ArgumentError('grams must be greater than 0');
|
||||
}
|
||||
if (grams <= 500) return 0.5;
|
||||
if (grams <= 1000) return 1.0;
|
||||
return (grams / 500).ceilToDouble() * 0.5;
|
||||
}
|
||||
|
||||
/// Health Check - Test if API is running
|
||||
static Future<bool> healthCheck() async {
|
||||
try {
|
||||
|
|
@ -203,6 +216,8 @@ class MLService {
|
|||
'production_quantity': productionQuantity,
|
||||
};
|
||||
|
||||
print('[consumeStock] Request payload: ${jsonEncode(data)}');
|
||||
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$baseUrl/stock/consume'),
|
||||
|
|
@ -211,6 +226,9 @@ class MLService {
|
|||
)
|
||||
.timeout(Duration(seconds: timeoutSeconds));
|
||||
|
||||
print('[consumeStock] Response status: ${response.statusCode}');
|
||||
print('[consumeStock] Response body: ${response.body}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import joblib
|
|||
import pandas as pd
|
||||
import numpy as np
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
import mysql.connector
|
||||
from mysql.connector import Error
|
||||
|
|
@ -20,6 +21,9 @@ logger = logging.getLogger(__name__)
|
|||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# Prevent overlapping stock consumption transactions
|
||||
transaction_in_progress = False
|
||||
|
||||
# ============================================================================
|
||||
# DATABASE CONFIGURATION
|
||||
# ============================================================================
|
||||
|
|
@ -59,6 +63,21 @@ def get_product_name_column(connection) -> str:
|
|||
cursor.close()
|
||||
return 'name' if has_name else 'product_name'
|
||||
|
||||
def grams_to_kg_rounded(grams: float) -> float:
|
||||
"""
|
||||
Convert grams to kilograms with rounding rules:
|
||||
- <= 500g -> 0.5kg
|
||||
- 500g < g <= 1000g -> 1kg
|
||||
- > 1000g -> round up to nearest 0.5kg
|
||||
"""
|
||||
if grams <= 0:
|
||||
raise ValueError("grams must be greater than 0")
|
||||
if grams <= 500:
|
||||
return 0.5
|
||||
if grams <= 1000:
|
||||
return 1.0
|
||||
return math.ceil(grams / 500.0) * 0.5
|
||||
|
||||
# ============================================================================
|
||||
# LOAD MODELS AT STARTUP
|
||||
# ============================================================================
|
||||
|
|
@ -449,8 +468,17 @@ def consume_stock():
|
|||
"allow_partial": false
|
||||
}
|
||||
"""
|
||||
global transaction_in_progress
|
||||
if transaction_in_progress:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Transaction already in progress'
|
||||
}), 429
|
||||
|
||||
transaction_in_progress = True
|
||||
try:
|
||||
data = request.json or {}
|
||||
logger.info(f"[stock/consume] Incoming payload: {data}")
|
||||
items = data.get('items', [])
|
||||
allow_partial = bool(data.get('allow_partial', False))
|
||||
|
||||
|
|
@ -477,6 +505,7 @@ def consume_stock():
|
|||
product_id = item.get('product_id')
|
||||
product_name = item.get('product_name')
|
||||
quantity = item.get('quantity')
|
||||
unit = (item.get('unit') or '').strip().lower()
|
||||
|
||||
if not isinstance(quantity, (int, float)) or quantity <= 0:
|
||||
errors.append({
|
||||
|
|
@ -504,7 +533,29 @@ def consume_stock():
|
|||
})
|
||||
continue
|
||||
|
||||
if product['current_stock'] < quantity:
|
||||
product_unit = (product.get('unit') or '').strip().lower()
|
||||
effective_quantity = quantity
|
||||
effective_unit = unit or product_unit
|
||||
|
||||
if unit in ['g', 'gram', 'grams']:
|
||||
try:
|
||||
effective_quantity = grams_to_kg_rounded(float(quantity))
|
||||
effective_unit = 'kg'
|
||||
logger.info(
|
||||
f"[stock/consume] Rounded grams to kg: {quantity}g -> {effective_quantity}kg (product_id={product.get('id')})"
|
||||
)
|
||||
except ValueError:
|
||||
errors.append({
|
||||
'item': item,
|
||||
'message': 'Quantity gram harus lebih dari 0'
|
||||
})
|
||||
continue
|
||||
else:
|
||||
logger.info(
|
||||
f"[stock/consume] Using quantity without gram rounding: {quantity} {effective_unit} (product_id={product.get('id')})"
|
||||
)
|
||||
|
||||
if product['current_stock'] < effective_quantity:
|
||||
errors.append({
|
||||
'item': item,
|
||||
'message': 'Stok tidak cukup',
|
||||
|
|
@ -514,8 +565,10 @@ def consume_stock():
|
|||
|
||||
valid_items.append({
|
||||
'product': product,
|
||||
'quantity': quantity,
|
||||
'unit': item.get('unit') or product.get('unit'),
|
||||
'quantity': effective_quantity,
|
||||
'unit': effective_unit or product.get('unit'),
|
||||
'input_quantity': quantity,
|
||||
'input_unit': unit or product.get('unit')
|
||||
})
|
||||
|
||||
if errors and not allow_partial:
|
||||
|
|
@ -558,7 +611,10 @@ def consume_stock():
|
|||
'product_id': product['id'],
|
||||
'product_name': product['name'],
|
||||
'quantity_used': quantity,
|
||||
'unit': entry['unit']
|
||||
'deducted_amount': quantity,
|
||||
'unit': entry['unit'],
|
||||
'quantity_input': entry.get('input_quantity'),
|
||||
'unit_input': entry.get('input_unit')
|
||||
})
|
||||
|
||||
connection.commit()
|
||||
|
|
@ -575,6 +631,8 @@ def consume_stock():
|
|||
except Exception as e:
|
||||
logger.error(f"Consume stock error: {str(e)}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
finally:
|
||||
transaction_in_progress = False
|
||||
|
||||
|
||||
@app.route('/transactions', methods=['POST'])
|
||||
|
|
|
|||
Loading…
Reference in New Issue