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) {
|
String getStockUnit(String ingredient) {
|
||||||
final unit = productUnits[ingredient];
|
final unit = productUnits[ingredient];
|
||||||
if (unit != null && unit.isNotEmpty) return unit;
|
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) {
|
double getRequiredInStockUnit(String ingredient) {
|
||||||
|
|
@ -269,23 +277,9 @@ class PredictionController extends ChangeNotifier {
|
||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
double _applyRoundingForStock(double amountInStockUnit, String stockUnit) {
|
double _roundRequiredGramToKg(double grams) {
|
||||||
final unit = stockUnit.toLowerCase();
|
|
||||||
if (unit != 'kg' && unit != 'gr') return amountInStockUnit;
|
|
||||||
|
|
||||||
final grams = unit == 'kg' ? amountInStockUnit * 1000 : amountInStockUnit;
|
|
||||||
if (grams <= 0) return 0;
|
if (grams <= 0) return 0;
|
||||||
|
return MLService.gramsToKgRounded(grams);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, double> get roundedUsage {
|
Map<String, double> get roundedUsage {
|
||||||
|
|
@ -294,24 +288,31 @@ class PredictionController extends ChangeNotifier {
|
||||||
|
|
||||||
required.forEach((ingredient, neededAmount) {
|
required.forEach((ingredient, neededAmount) {
|
||||||
if (!isIngredientSelected(ingredient)) return;
|
if (!isIngredientSelected(ingredient)) return;
|
||||||
final stockUnit = getStockUnit(ingredient);
|
final unit = getIngredientUnit(ingredient);
|
||||||
final requiredInStockUnit = _convertToStockUnit(
|
final requiredGram = toGram(amount: neededAmount, unit: unit);
|
||||||
ingredient: ingredient,
|
rounded[ingredient] = _roundRequiredGramToKg(requiredGram);
|
||||||
amount: neededAmount,
|
|
||||||
fromUnit: getIngredientUnit(ingredient),
|
|
||||||
);
|
|
||||||
rounded[ingredient] = _applyRoundingForStock(
|
|
||||||
requiredInStockUnit,
|
|
||||||
stockUnit,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return rounded;
|
return rounded;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitProduction() async {
|
Future<Map<String, dynamic>> submitProduction() async {
|
||||||
|
if (isSubmitting) {
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Permintaan sedang diproses, mohon tunggu',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
if (selectedRecipe == null || !isCalculated) {
|
if (selectedRecipe == null || !isCalculated) {
|
||||||
return {'status': 'error', 'message': 'Hitung kebutuhan terlebih dahulu'};
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Hitung kebutuhan terlebih dahulu',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshStock();
|
await refreshStock();
|
||||||
|
|
@ -327,19 +328,15 @@ class PredictionController extends ChangeNotifier {
|
||||||
return {'status': 'error', 'message': 'Tidak ada bahan yang dipilih'};
|
return {'status': 'error', 'message': 'Tidak ada bahan yang dipilih'};
|
||||||
}
|
}
|
||||||
|
|
||||||
isSubmitting = true;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final items =
|
final items =
|
||||||
rounded.entries.map((entry) {
|
rounded.entries.where((entry) => entry.value > 0).map((entry) {
|
||||||
final ingredient = entry.key;
|
final ingredient = entry.key;
|
||||||
final quantity = entry.value;
|
final quantityKg = entry.value;
|
||||||
return {
|
return {
|
||||||
'product_id': productIds[ingredient],
|
'product_id': productIds[ingredient],
|
||||||
'product_name': ingredient,
|
'product_name': ingredient,
|
||||||
'quantity': quantity,
|
'quantity': quantityKg,
|
||||||
'unit': getStockUnit(ingredient),
|
'unit': 'kg',
|
||||||
};
|
};
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
|
@ -359,6 +356,7 @@ class PredictionController extends ChangeNotifier {
|
||||||
double.infinity,
|
double.infinity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await refreshStock();
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _submitProduction() async {
|
Future<void> _submitProduction() async {
|
||||||
|
if (_controller.isSubmitting) return;
|
||||||
final result = await _controller.submitProduction();
|
final result = await _controller.submitProduction();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
|
@ -606,8 +607,6 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
||||||
_controller.getIngredientUnit(
|
_controller.getIngredientUnit(
|
||||||
ingredient,
|
ingredient,
|
||||||
);
|
);
|
||||||
final stockUnit = _controller
|
|
||||||
.getStockUnit(ingredient);
|
|
||||||
final quantityPerUnit =
|
final quantityPerUnit =
|
||||||
(details['quantity'] as num)
|
(details['quantity'] as num)
|
||||||
.toDouble();
|
.toDouble();
|
||||||
|
|
@ -617,18 +616,24 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
||||||
_controller
|
_controller
|
||||||
.productionQuantity
|
.productionQuantity
|
||||||
: 0.0;
|
: 0.0;
|
||||||
final availableAmount =
|
final stockKg =
|
||||||
_controller
|
_controller
|
||||||
.currentStock[ingredient] ??
|
.currentStock[ingredient] ??
|
||||||
0.0;
|
0.0;
|
||||||
|
final requiredGram =
|
||||||
|
isSelected
|
||||||
|
? _controller.toGram(
|
||||||
|
amount: neededAmount,
|
||||||
|
unit: unit,
|
||||||
|
)
|
||||||
|
: 0.0;
|
||||||
final requiredInStockUnit =
|
final requiredInStockUnit =
|
||||||
_controller
|
_controller
|
||||||
.getRequiredInStockUnit(
|
.getRequiredInStockUnit(
|
||||||
ingredient,
|
ingredient,
|
||||||
);
|
);
|
||||||
final isSufficient =
|
final isSufficient =
|
||||||
availableAmount >=
|
stockKg >= requiredInStockUnit;
|
||||||
requiredInStockUnit;
|
|
||||||
final statusColor =
|
final statusColor =
|
||||||
isSelected
|
isSelected
|
||||||
? (isSufficient
|
? (isSufficient
|
||||||
|
|
@ -720,7 +725,7 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${_controller.formatQuantity(neededAmount)} $unit',
|
'${_controller.formatQuantity(requiredGram)} gr',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize:
|
fontSize:
|
||||||
12,
|
12,
|
||||||
|
|
@ -750,7 +755,7 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${_controller.formatQuantity(availableAmount)} $stockUnit',
|
'${_controller.formatQuantity(stockKg)} kg',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize:
|
fontSize:
|
||||||
12,
|
12,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,23 @@ class MLService {
|
||||||
// API URL - Change based on environment
|
// API URL - Change based on environment
|
||||||
// Untuk emulator Android: 10.0.2.2
|
// Untuk emulator Android: 10.0.2.2
|
||||||
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
// 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;
|
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
|
/// Health Check - Test if API is running
|
||||||
static Future<bool> healthCheck() async {
|
static Future<bool> healthCheck() async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -203,6 +216,8 @@ class MLService {
|
||||||
'production_quantity': productionQuantity,
|
'production_quantity': productionQuantity,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
print('[consumeStock] Request payload: ${jsonEncode(data)}');
|
||||||
|
|
||||||
final response = await http
|
final response = await http
|
||||||
.post(
|
.post(
|
||||||
Uri.parse('$baseUrl/stock/consume'),
|
Uri.parse('$baseUrl/stock/consume'),
|
||||||
|
|
@ -211,6 +226,9 @@ class MLService {
|
||||||
)
|
)
|
||||||
.timeout(Duration(seconds: timeoutSeconds));
|
.timeout(Duration(seconds: timeoutSeconds));
|
||||||
|
|
||||||
|
print('[consumeStock] Response status: ${response.statusCode}');
|
||||||
|
print('[consumeStock] Response body: ${response.body}');
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import joblib
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
|
|
@ -20,6 +21,9 @@ logger = logging.getLogger(__name__)
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
|
# Prevent overlapping stock consumption transactions
|
||||||
|
transaction_in_progress = False
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DATABASE CONFIGURATION
|
# DATABASE CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -59,6 +63,21 @@ def get_product_name_column(connection) -> str:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
return 'name' if has_name else 'product_name'
|
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
|
# LOAD MODELS AT STARTUP
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -449,8 +468,17 @@ def consume_stock():
|
||||||
"allow_partial": false
|
"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:
|
try:
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
|
logger.info(f"[stock/consume] Incoming payload: {data}")
|
||||||
items = data.get('items', [])
|
items = data.get('items', [])
|
||||||
allow_partial = bool(data.get('allow_partial', False))
|
allow_partial = bool(data.get('allow_partial', False))
|
||||||
|
|
||||||
|
|
@ -477,6 +505,7 @@ def consume_stock():
|
||||||
product_id = item.get('product_id')
|
product_id = item.get('product_id')
|
||||||
product_name = item.get('product_name')
|
product_name = item.get('product_name')
|
||||||
quantity = item.get('quantity')
|
quantity = item.get('quantity')
|
||||||
|
unit = (item.get('unit') or '').strip().lower()
|
||||||
|
|
||||||
if not isinstance(quantity, (int, float)) or quantity <= 0:
|
if not isinstance(quantity, (int, float)) or quantity <= 0:
|
||||||
errors.append({
|
errors.append({
|
||||||
|
|
@ -504,7 +533,29 @@ def consume_stock():
|
||||||
})
|
})
|
||||||
continue
|
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({
|
errors.append({
|
||||||
'item': item,
|
'item': item,
|
||||||
'message': 'Stok tidak cukup',
|
'message': 'Stok tidak cukup',
|
||||||
|
|
@ -514,8 +565,10 @@ def consume_stock():
|
||||||
|
|
||||||
valid_items.append({
|
valid_items.append({
|
||||||
'product': product,
|
'product': product,
|
||||||
'quantity': quantity,
|
'quantity': effective_quantity,
|
||||||
'unit': item.get('unit') or product.get('unit'),
|
'unit': effective_unit or product.get('unit'),
|
||||||
|
'input_quantity': quantity,
|
||||||
|
'input_unit': unit or product.get('unit')
|
||||||
})
|
})
|
||||||
|
|
||||||
if errors and not allow_partial:
|
if errors and not allow_partial:
|
||||||
|
|
@ -558,7 +611,10 @@ def consume_stock():
|
||||||
'product_id': product['id'],
|
'product_id': product['id'],
|
||||||
'product_name': product['name'],
|
'product_name': product['name'],
|
||||||
'quantity_used': quantity,
|
'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()
|
connection.commit()
|
||||||
|
|
@ -575,6 +631,8 @@ def consume_stock():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Consume stock error: {str(e)}")
|
logger.error(f"Consume stock error: {str(e)}")
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
transaction_in_progress = False
|
||||||
|
|
||||||
|
|
||||||
@app.route('/transactions', methods=['POST'])
|
@app.route('/transactions', methods=['POST'])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue