Formulasi Resep & Biaya
Real-TimeKatalog Menu & Resep Tersimpan
Daftar seluruh formula HPP dan harga jual yang telah dihitung dan disimpan.
Master Database Bahan Baku & Kemasan
Daftar harga beli bahan dan konversi otomatis harga per gram, ml, atau pcs.
| Nama Bahan / Kemasan | Kategori | Satuan Beli | Total Harga Beli | Harga Pokok Terkecil | Aksi |
|---|
Integrasi Google Spreadsheet
Hubungkan kalkulator ini dengan Google Spreadsheet milik Anda menggunakan Google Apps Script Web App gratis tanpa server berbayar.
📋 Panduan Mudah Memasang Google Apps Script (2 Menit Selesai)
Buat Google Spreadsheet Baru
Buka tab baru dan buat Google Sheets di sheets.new. Beri judul spreadsheet Anda, misal: "Database HPP Kopi & Bakery".
Buka Menu Ekstensi > Apps Script
Di menu bar Google Sheets, klik menu Extensions (Ekstensi) > lalu klik Apps Script.
Salin & Tempel Kode Script Berikut ke File Code.gs
Hapus seluruh isi default file Code.gs di Apps Script, lalu klik tombol "Salin Seluruh Kode Script" di bawah dan paste ke sana:
/**
* BACKEND GOOGLE APPS SCRIPT: KALKULATOR HPP KOPI & BAKERY
*/
const SHEET_NAMES = {
INGREDIENTS: "BahanBaku",
RECIPES: "ResepHPP",
LOGS: "LogRiwayat"
};
function doGet(e) {
try {
const action = e.parameter.action || "getData";
if (action === "ping") {
return createJsonResponse({ status: "success", message: "Koneksi Google Spreadsheet Aktif!" });
}
if (action === "getData") {
return createJsonResponse({ status: "success", data: getAllDatabase() });
}
return createJsonResponse({ status: "error", message: "Action tidak dikenali" });
} catch (error) {
return createJsonResponse({ status: "error", message: error.toString() });
}
}
function doPost(e) {
try {
let payload = {};
if (e.postData && e.postData.contents) {
payload = JSON.parse(e.postData.contents);
}
const action = payload.action || "syncAll";
if (action === "syncAll") {
syncAllData(payload.ingredients || [], payload.recipes || []);
return createJsonResponse({ status: "success", message: "Sinkronisasi Berhasil ke Google Sheets!" });
}
return createJsonResponse({ status: "error", message: "Action tidak dikenali" });
} catch (error) {
return createJsonResponse({ status: "error", message: error.toString() });
}
}
function createJsonResponse(data) {
return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON);
}
function getOrCreateSheet(sheetName, headers) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(sheetName);
if (!sheet) {
sheet = ss.insertSheet(sheetName);
if (headers) {
const headerRange = sheet.getRange(1, 1, 1, headers.length);
headerRange.setValues([headers]);
headerRange.setBackground("#3D2618").setFontColor("#FFFFFF").setFontWeight("bold").setHorizontalAlignment("center");
sheet.setFrozenRows(1);
}
}
return sheet;
}
function getAllDatabase() {
const ingSheet = getOrCreateSheet(SHEET_NAMES.INGREDIENTS, ["ID", "Nama Bahan Baku / Kemasan", "Kategori", "Satuan", "Jumlah Beli", "Harga Beli (Rp)", "Harga Per Satuan (Rp)", "Catatan", "Terakhir Diperbarui"]);
const ingData = ingSheet.getDataRange().getValues();
const ingredients = [];
for (let i = 1; i < ingData.length; i++) {
const row = ingData[i];
if (row[0] || row[1]) {
ingredients.push({ id: String(row[0]), name: String(row[1]), category: String(row[2]), unit: String(row[3]), purchaseQty: Number(row[4]), purchasePrice: Number(row[5]), unitPrice: Number(row[6]), notes: String(row[7] || "") });
}
}
const recSheet = getOrCreateSheet(SHEET_NAMES.RECIPES, ["ID Resep", "Nama Resep / Menu", "Kategori", "Hasil Batch", "Satuan Porsi", "Biaya Bahan Baku (Rp)", "Biaya Kemasan (Rp)", "Biaya Overhead (Rp)", "Total HPP Batch (Rp)", "HPP Per Porsi (Rp)", "Target Margin %", "Harga Jual Rekomendasi (Rp)", "Profit Per Porsi (Rp)", "Food Cost %", "Detail Komposisi (JSON)", "Terakhir Diperbarui"]);
const recData = recSheet.getDataRange().getValues();
const recipes = [];
for (let i = 1; i < recData.length; i++) {
const row = recData[i];
if (row[0] || row[1]) {
let details = { ingredients: [], packaging: [] };
try { if (row[14]) details = JSON.parse(row[14]); } catch (e) {}
recipes.push({ id: String(row[0]), name: String(row[1]), category: String(row[2]), batchYield: Number(row[3]), yieldUnit: String(row[4]), ingredientsTotal: Number(row[5]), packagingTotal: Number(row[6]), overheadTotal: Number(row[7]), totalBatchHpp: Number(row[8]), hppPerUnit: Number(row[9]), targetMargin: Number(row[10]), finalSellingPrice: Number(row[11]), profitPerUnit: Number(row[12]), foodCostRatio: Number(row[13]), ingredients: details.ingredients || [], packaging: details.packaging || [], gasCost: Number(details.gasCost) || 0, electricityCost: Number(details.electricityCost) || 0, waterCost: Number(details.waterCost) || 0, parkingCost: Number(details.parkingCost) || 0, petrolCost: Number(details.petrolCost) || 0, otherOverheadNominal: Number(details.otherOverheadNominal) || 0, overheadRate: details.overheadRate !== undefined ? details.overheadRate : 5 });
}
}
return { ingredients, recipes };
}
function syncAllData(ingredients, recipes) {
if (ingredients.length > 0) {
const ingSheet = getOrCreateSheet(SHEET_NAMES.INGREDIENTS, ["ID", "Nama Bahan Baku / Kemasan", "Kategori", "Satuan", "Jumlah Beli", "Harga Beli (Rp)", "Harga Per Satuan (Rp)", "Catatan", "Terakhir Diperbarui"]);
const lastRow = ingSheet.getLastRow();
if (lastRow > 1) ingSheet.getRange(2, 1, lastRow - 1, 9).clearContent();
const rows = ingredients.map(ing => [ing.id, ing.name, ing.category, ing.unit, ing.purchaseQty, ing.purchasePrice, ing.unitPrice, ing.notes || "", new Date().toLocaleString("id-ID")]);
ingSheet.getRange(2, 1, rows.length, 9).setValues(rows);
}
if (recipes.length > 0) {
const recSheet = getOrCreateSheet(SHEET_NAMES.RECIPES, ["ID Resep", "Nama Resep / Menu", "Kategori", "Hasil Batch", "Satuan Porsi", "Biaya Bahan Baku (Rp)", "Biaya Kemasan (Rp)", "Biaya Overhead (Rp)", "Total HPP Batch (Rp)", "HPP Per Porsi (Rp)", "Target Margin %", "Harga Jual Rekomendasi (Rp)", "Profit Per Porsi (Rp)", "Food Cost %", "Detail Komposisi (JSON)", "Terakhir Diperbarui"]);
const lastRow = recSheet.getLastRow();
if (lastRow > 1) recSheet.getRange(2, 1, lastRow - 1, 16).clearContent();
const rows = recipes.map(r => [r.id, r.name, r.category, r.batchYield, r.yieldUnit, r.ingredientsTotal, r.packagingTotal, r.overheadTotal, r.totalBatchHpp, r.hppPerUnit, r.targetMargin, r.finalSellingPrice, r.profitPerUnit, r.foodCostRatio, JSON.stringify({ ingredients: r.ingredients, packaging: r.packaging, notes: r.notes, gasCost: r.gasCost, electricityCost: r.electricityCost, waterCost: r.waterCost, parkingCost: r.parkingCost, petrolCost: r.petrolCost, otherOverheadNominal: r.otherOverheadNominal, overheadRate: r.overheadRate }), new Date().toLocaleString("id-ID")]);
recSheet.getRange(2, 1, rows.length, 16).setValues(rows);
}
}
Deploy sebagai Web App
Di pojok kanan atas editor Apps Script, klik tombol Deploy (Terapkan) > pilih New deployment (Penerapan baru).
1. Klik ikon gerigi (Select type) lalu pilih Web app.
2. Execute as: pilih Me (email Anda).
3. Who has access: pilih Anyone (Siapa saja).
4. Klik tombol Deploy. Salin Web App URL yang dihasilkan (berakhiran /exec), lalu tempelkan ke kolom URL di atas!
Simulasi Sensitivitas Biaya & Break Even Point (BEP)
Lakukan proyeksi skenario jika harga bahan baku di pasar naik, serta hitung berapa gelas kopi/kue yang harus terjual per hari untuk menutup biaya sewa dan gaji karyawan.
Simulasi Fluktuasi Bahan Utama
Jika bahan baku naik sebesar persentase di atas, Anda disarankan menaikkan harga jual atau menyesuaikan resep agar margin tetap aman di atas 60%.