(function ($) {
"use strict";
var cfg = window.INVOICE_PAGE || {},
lineTable = null,
selectedItem = null,
selectedBars = [],
batchLines = [];
function esc(v) {
return $("
")
.text(v == null ? "" : v)
.html();
}
function parseMoney(v) {
v = String(v == null ? "" : v).replace(/Rp|\s/g, "");
if (v.indexOf(",") >= 0) v = v.replace(/\./g, "").replace(",", ".");
else if (/^\d{1,3}(\.\d{3})+$/.test(v)) v = v.replace(/\./g, "");
return Number(v) || 0;
}
function money(v) {
return (
"Rp " +
new Intl.NumberFormat("id-ID", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(Number(v) || 0)
);
}
function qty(v) {
v = Number(v) || 0;
return new Intl.NumberFormat("id-ID", { maximumFractionDigits: 4 }).format(
v,
);
}
function fmt(v) {
return new Intl.NumberFormat("id-ID", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(Number(v) || 0);
}
function token() {
return (
window.crypto && crypto.randomUUID
? crypto.randomUUID()
: Date.now() + "-" + Math.random().toString(16).slice(2)
).replace(/[^a-zA-Z0-9-]/g, "");
}
function notify(message, icon) {
if (window.AppAlert) return AppAlert.toast(message, icon || "success");
return Swal.fire({
toast: true,
position: "top-end",
icon: icon || "success",
title: message,
showConfirmButton: false,
timer: 3200,
});
}
function error(xhr) {
var message = "Permintaan tidak dapat diproses.";
try {
var r =
typeof xhr === "object" && xhr.responseJSON
? xhr.responseJSON
: JSON.parse(xhr.responseText);
message = r.message || message;
} catch (e) {}
notify(message, "error");
}
function post(url, data) {
data = data || {};
if (data instanceof FormData) {
if (!data.has(cfg.csrfName)) data.append(cfg.csrfName, cfg.csrfHash);
} else data[cfg.csrfName] = cfg.csrfHash;
return $.ajax({
url: url,
type: "POST",
data: data,
dataType: "json",
processData: !(data instanceof FormData),
contentType:
data instanceof FormData
? false
: "application/x-www-form-urlencoded; charset=UTF-8",
});
}
function confirmAction(options) {
return window.AppAlert
? AppAlert.confirm(options)
: Swal.fire(
Object.assign(
{ icon: "warning", position: "center", showCancelButton: true },
options,
),
);
}
function statusLabel(v) {
var labels = {
draft: "Draft",
submitted: "Menunggu Approval",
approved: "Disetujui",
posted: "Posted",
unpaid: "Belum Lunas",
partial: "Sebagian",
paid: "Lunas",
not_delivered: "Belum Dikirim",
delivered: "Sudah Dikirim",
one_time: "Sekali Transaksi",
running: "Berjalan / Akumulasi",
};
return labels[v] || String(v || "-").replace(/_/g, " ");
}
function invoiceButtons(row) {
var p = cfg.permissions || {},
isDraft = row.workflow_status === "draft",
isPaid = row.status === "paid",
html = '
';
if (!isPaid && (!isDraft || !p.update))
html +=
'
Detail';
if (!isPaid && isDraft && p.update)
html +=
'
Kelola';
if (
!isPaid &&
isDraft &&
Number(row.pending_delivery_count || 0) > 0 &&
p.update
)
html +=
'
';
if (p.export)
html +=
'
';
if (!isPaid && isDraft && p.submit)
html +=
'
';
if (!isPaid && row.workflow_status === "submitted" && p.approve)
html +=
'
';
if (
!isPaid &&
row.workflow_status === "posted" &&
["unpaid", "partial"].indexOf(row.status) >= 0 &&
p.post
)
html +=
'
Bayar';
if (!isPaid && isDraft && p.delete)
html +=
'
';
return html + "
";
}
function initList() {
if (!$("#invoiceTable").length) return;
var params = new URLSearchParams(location.search);
$("#filterWorkflow").val(params.get("workflow_status") || "");
$("#filterPayment").val(params.get("payment_status") || "");
var table = $("#invoiceTable").DataTable({
processing: true,
serverSide: true,
searchDelay: 350,
pageLength: 25,
order: [],
responsive: false,
ajax: function (data, callback) {
data.workflow_status = $("#filterWorkflow").val();
data.payment_status = $("#filterPayment").val();
data.date_from = $("#filterFrom").val();
data.date_to = $("#filterTo").val();
$.ajax({
url: cfg.base + "invoices/data",
data: data,
dataType: "json",
})
.done(function (r) {
callback(r);
})
.fail(function (x) {
error(x);
callback({
draw: data.draw,
recordsTotal: 0,
recordsFiltered: 0,
data: [],
});
});
},
columns: [
{
data: null,
render: function (d, t, r) {
return (
"
" +
esc(r.no_invoice) +
"" +
esc(r.customer_name) +
""
);
},
},
{
data: "jatuh_tempo",
render: function (v) {
return esc(v || "-");
},
},
{
data: null,
render: function (d,t,r) {
if(r.invoice_purpose==="loan")return "Tagihan Pinjaman";
if(r.invoice_purpose==="savings")return "Tagihan Tabungan";
if(r.invoice_purpose==="mixed")return "Penjualan + Tabungan";
return esc(
r.recognition_policy === "accrual"
? "Diakui saat Posted"
: "Proporsional Setelah Dibayar",
);
},
},
{
data: "total",
className: "text-end",
render: function (v) {
return money(v);
},
},
{
data: "total_bayar",
className: "text-end",
render: function (v) {
return money(v);
},
},
{
data: "sisa_piutang",
className: "text-end",
render: function (v) {
return "
" + money(v) + "";
},
},
{
data: null,
render: function (d, t, r) {
return (
'
' +
statusLabel(r.workflow_status) +
"" +
statusLabel(r.status) +
""
);
},
},
{
data: null,
render: function (d,t,r) {
return Number(r.inventory_line_count||0)>0?statusLabel(r.delivery_status):"Tidak Memerlukan";
},
},
{ data: "creator_name", defaultContent: "-" },
{
data: null,
orderable: false,
searchable: false,
render: function (d, t, r) {
return invoiceButtons(r);
},
},
],
language: {
processing: "Memuat invoice...",
search: "Cari:",
lengthMenu: "Tampilkan _MENU_",
info: "_START_–_END_ dari _TOTAL_ invoice",
infoEmpty: "Belum ada invoice",
zeroRecords: "Invoice tidak ditemukan",
paginate: { previous: "Sebelumnya", next: "Berikutnya" },
},
});
$("#applyInvoiceFilter").on("click", function () {
table.ajax.reload();
});
$("#resetInvoiceFilter").on("click", function () {
$("#invoiceFilters select,#invoiceFilters input").val("");
table.search("").ajax.reload();
});
$("#toggleInvoiceFilters").on("click", function () {
$("#invoiceFilters").toggleClass("open");
});
}
function select2Ajax(selector, url, placeholder, extra) {
var element = $(selector);
if (!element.length) return;
var options = {
width: "100%",
placeholder: placeholder,
allowClear: true,
minimumInputLength: 0,
ajax: {
url: cfg.base + url,
dataType: "json",
delay: 300,
data: function (p) {
return Object.assign(
{ q: p.term || "", page: p.page || 1 },
extra ? extra() : {},
);
},
processResults: function (r) {
return {
results: r.results || [],
pagination: r.pagination || { more: false },
};
},
cache: true,
},
};
if (element.closest(".modal").length)
options.dropdownParent = element.closest(".modal");
element.select2(options);
}
function initEditor() {
if (cfg.mode === "list") return;
select2Ajax(".ajax-customer", "invoices/lookup_customers", "Cari customer");
select2Ajax(
".ajax-account",
"invoices/lookup_accounts",
"Pilih akun pendapatan",
);
select2Ajax(
".ajax-liability-account",
"invoices/lookup_accounts",
"Pilih akun simpanan Passiva",
function () { return { scope: "liability" }; },
);
select2Ajax(
".ajax-item",
"invoices/lookup_items",
"Cari kode atau nama barang",
);
select2Ajax(
".ajax-warehouse",
"invoices/lookup_warehouses",
"Pilih gudang",
);
select2Ajax(
".ajax-barcode",
"invoices/lookup_barcodes",
"Cari barcode atau serial",
function () {
return {
item_id: $(".ajax-item").val() || 0,
warehouse_id: $(".ajax-warehouse").val() || 0,
editing_line_id: Number($("#invoiceLineForm [name=editing_line_id]").val()) || 0,
};
},
);
$(".ajax-customer")
.on("select2:select", function (e) {
showCustomerPreview(e.params.data);
})
.on("select2:clear", function () {
$("#selectedCustomerPreview").addClass("d-none").empty();
});
if (cfg.sourceOrder) {
$.getJSON(cfg.base + "salesdocuments/detail/" + cfg.sourceOrder)
.done(function (r) {
if (!r.status) return notify(r.message, "error");
var c = r.document;
var option = new Option(c.customer_name, c.customer_id, true, true);
$(".ajax-customer").append(option).trigger("change");
showCustomerPreview({
text: c.customer_name,
alamat: c.customer_address || c.address || "",
});
notify("Customer diambil dari Sales Order " + c.document_no, "info");
})
.fail(error);
}
$("#invoiceHeaderForm").on("submit", function (e) {
e.preventDefault();
var form = this,
button = $(form).find(":submit");
button
.prop("disabled", true)
.html(
'
Menyimpan...',
);
post(cfg.base + "invoices/create_header", new FormData(form))
.done(function (r) {
if (!r.status) {
notify(r.message, "error");
return;
}
notify(r.message, "success");
location.href = cfg.base + "invoices/edit/" + r.result;
})
.fail(error)
.always(function () {
button
.prop("disabled", false)
.html('
Simpan Invoice & Lanjut');
});
});
if (!cfg.invoiceId) return;
initLineTable();
bindLineForm();
bindEditorActions();
}
function showCustomerPreview(customer) {
if (!customer) return;
$("#selectedCustomerPreview")
.removeClass("d-none")
.html(
"
" +
esc(customer.text || customer.nama || "Customer") +
"" +
esc(customer.alamat || "Alamat belum tersedia") +
"",
);
}
function bindLineForm() {
var form = $("#invoiceLineForm");
if (!form.length) return;
$("input[name=line_type]").on("change", function () {
var type = this.value,
inventory = type === "inventory",
savings = type === "savings";
$(".inventory-only").toggleClass("d-none", !inventory);
$(".noninventory-only").toggleClass("d-none", inventory);
$(".savings-only").toggleClass("d-none", !savings);
$(".sales-account-field,.sales-tax-field,.service-unit-field,.line-qty-field").toggleClass("d-none", savings);
$(".invoice-batch-common").toggleClass("d-none", savings);
$("#nonInventoryDescriptionLabel").html(savings ? 'Deskripsi
*' : 'Nama Jasa / Deskripsi
*');
$("#lineValueLabel").text(savings ? "Nominal" : "Harga Satuan / Nominal");
if(savings){form.find("[name=tax_rate]").val("0");form.find("[name=qty]").val("1");form.find("[name=unit]").val("Tabungan");}
else if(!inventory)form.find("[name=unit]").val("Jasa");
selectedItem = null;
selectedBars = [];
$(".ajax-item,.ajax-warehouse,.ajax-barcode").val(null).trigger("change");
renderBars();
calcBatch();
});
$(".ajax-item").on("select2:select", function (e) {
selectedItem = e.params.data;
form.find("[name=unit_price]").val(fmt(selectedItem.harga_jual));
$("#trackingHint").text(
(selectedItem.tracking_type || "UNIT") +
" - " +
(selectedItem.unit || "-"),
);
selectedBars = [];
$(".ajax-barcode").val(null).trigger("change");
renderBars();
calcBatch();
});
$(".ajax-warehouse").on("change", function () {
selectedBars = [];
$(".ajax-barcode").val(null).trigger("change");
renderBars();
calcBatch();
});
$(".ajax-barcode")
.on("select2:select", function (e) {
addBar(e.params.data);
})
.on("select2:unselect", function (e) {
selectedBars = selectedBars.filter(function (x) {
return String(x.id) !== String(e.params.data.id);
});
renderBars();
syncQtyFromBars();
});
$("#scanBarcodeButton").on("click", scanExact);
$("#barcodeScan").on("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
scanExact();
}
});
$(document).on("input", ".selected-barcode-qty", function () {
var id = $(this).data("id"),
bar = selectedBars.find(function (x) {
return String(x.id) === String(id);
});
if (bar)
bar.qty = Math.min(parseMoney(this.value), Number(bar.available));
syncQtyFromBars();
});
$(document).on("click", ".remove-selected-barcode", function () {
var id = $(this).data("id");
selectedBars = selectedBars.filter(function (x) {
return String(x.id) !== String(id);
});
$('.ajax-barcode option[value="' + id + '"]').prop("selected", false);
$(".ajax-barcode").trigger("change.select2");
renderBars();
syncQtyFromBars();
});
$(document).on("click", ".remove-batch-line", function () {
batchLines.splice(Number($(this).data("index")), 1);
renderBatchLines();
});
$("#stageInvoiceLine").on("click", stageCurrentLine);
form.on(
"input",
"[name=qty],[name=unit_price],[name=tax_rate],[name=batch_discount]",
calcBatch,
);
form.on("submit", function (e) {
e.preventDefault();
if (Number(form.find("[name=editing_line_id]").val()) > 0) {
stageCurrentLine();
return;
}
if (!batchLines.length)
return notify("Masukkan minimal satu item ke daftar.", "warning");
var date = form.find("[name=service_date]").val(),
discount = parseMoney(form.find("[name=batch_discount]").val()),
gross = batchLines.reduce(function (s, x) {
return s + (x.line_type === "savings" ? 0 : x.qty * x.unit_price);
}, 0);
if (!date) return notify("Tanggal transaksi wajib diisi.", "warning");
if (discount < 0 || discount > gross)
return notify(
"Diskon tidak boleh melebihi nilai bruto seluruh item.",
"warning",
);
var rows = batchLines.map(function (line) {
return {
line_type: line.line_type,
account_id: line.account_id,
counter_account_id: line.counter_account_id || "",
item_id: line.item_id || "",
warehouse_id: line.warehouse_id || "",
description: line.description || "",
unit: line.unit || "",
qty: line.qty,
unit_price: line.unit_price,
tax_rate: line.tax_rate,
notes: line.notes || "",
barcodes: line.barcodes || [],
};
});
var data = new FormData();
data.append("service_date", date);
data.append("batch_discount", discount);
data.append("idempotency_key", $("#lineIdempotency").val());
data.append("rows", JSON.stringify(rows));
var button = form.find(".batch-submit-button");
button
.prop("disabled", true)
.html(
'
Menyimpan...',
);
post(cfg.base + "invoices/add_lines/" + cfg.invoiceId, data)
.done(function (r) {
if (!r.status) return notify(r.message, "error");
notify(r.message, "success");
batchLines = [];
form.find("[name=batch_discount]").val("0");
$("#lineIdempotency").val(token());
resetComposer();
renderBatchLines();
if (lineTable) lineTable.ajax.reload(null, false);
bootstrap.Modal.getOrCreateInstance(
document.getElementById("invoiceItemModal"),
).hide();
})
.fail(error)
.always(function () {
renderBatchLines();
});
});
$(".money-input").on("blur", function () {
this.value = fmt(parseMoney(this.value));
});
renderBatchLines();
}
function stageCurrentLine() {
var form = $("#invoiceLineForm"),
type = form.find("[name=line_type]:checked").val(),
account = type === "savings" ? form.find("[name=savings_account_id]") : form.find("[name=revenue_account_id]"),
line = {
line_type: type,
account_id: Number(account.val()),
account_label: account.find(":selected").text(),
counter_account_id: 0,
counter_account_label: "",
qty: type === "savings" ? 1 : parseMoney(form.find("[name=qty]").val()),
unit_price: parseMoney(form.find("[name=unit_price]").val()),
tax_rate: Number(form.find("[name=tax_rate]").val()) || 0,
notes: String(form.find("[name=notes]").val() || "").trim(),
};
if (!line.account_id)
return notify(type === "savings" ? "Pilih akun simpanan customer terlebih dahulu." : "Pilih akun pendapatan terlebih dahulu.", "warning");
line.account_display = line.line_type === "loan" ? line.account_label + " ← " + line.counter_account_label : line.account_label;
if (!form.find("[name=service_date]").val())
return notify("Tanggal transaksi wajib diisi.", "warning");
if (line.qty <= 0) return notify("Qty harus lebih dari nol.", "warning");
if (line.unit_price < 0)
return notify("Harga satuan tidak valid.", "warning");
if (line.tax_rate < 0) return notify("Pajak tidak valid.", "warning");
if (batchLines.length >= 100)
return notify("Maksimal 100 item dalam satu kali penambahan.", "warning");
if (type === "inventory") {
if (!selectedItem || !form.find("[name=warehouse_id]").val())
return notify("Pilih gudang dan barang.", "warning");
if (!selectedBars.length)
return notify("Pilih minimal satu barcode barang.", "warning");
var barcodeQty = selectedBars.reduce(function (s, x) {
return s + Number(x.qty || 0);
}, 0);
if (Math.abs(barcodeQty - line.qty) > 0.0001)
return notify(
"Total qty barcode harus sama dengan qty barang.",
"warning",
);
line.item_id = Number(selectedItem.id);
line.warehouse_id = Number(form.find("[name=warehouse_id]").val());
line.description =
selectedItem.nama_barang || selectedItem.text || "Barang";
line.unit = selectedItem.unit || "";
line.label =
(selectedItem.kode_detail ? selectedItem.kode_detail + " - " : "") +
(selectedItem.nama_barang || selectedItem.text || "Barang");
line.barcodes = selectedBars.map(function (x) {
return {
id: Number(x.id),
qty: Number(x.qty),
barcode: x.barcode,
serial_number: x.serial_number || "",
};
});
} else {
line.description = String(
form.find("[name=description]").val() || "",
).trim();
line.unit = String(form.find("[name=unit]").val() || "Jasa").trim();
line.label = line.description;
if (!line.description)
return notify("Nama jasa atau deskripsi wajib diisi.", "warning");
line.barcodes = [];
}
if(type === "savings")line.tax_rate=0;
if(Number(form.find("[name=editing_line_id]").val())>0)return updateExistingLine(line);
batchLines.push(line);
resetComposer();
renderBatchLines();
notify("Item dimasukkan ke daftar.", "success");
}
function updateExistingLine(line){
var form=$("#invoiceLineForm"),id=Number(form.find("[name=editing_line_id]").val()),button=$("#stageInvoiceLine"),data={line_type:line.line_type,service_date:form.find("[name=service_date]").val(),description:line.description,unit:line.unit,qty:line.qty,unit_price:line.unit_price,discount_amount:parseMoney(form.find("[name=batch_discount]").val()),tax_rate:line.tax_rate,notes:line.notes,account_id:line.account_id,counter_account_id:line.counter_account_id||"",item_id:line.item_id||"",warehouse_id:line.warehouse_id||"",barcodes:JSON.stringify(line.barcodes||[])};
button.prop("disabled",true).html('
Menyimpan...');
post(cfg.base+"invoices/update_line/"+cfg.invoiceId+"/"+id,data).done(function(r){if(!r.status)return notify(r.message,"error");notify(r.message,"success");var refreshPage=Number(form.data("editing-delivery")||0)>0;bootstrap.Modal.getOrCreateInstance(document.getElementById("invoiceItemModal")).hide();if(refreshPage)setTimeout(function(){location.reload();},350);else if(lineTable)lineTable.ajax.reload(null,false)}).fail(error).always(function(){button.prop("disabled",false).html('
Simpan Perubahan')});
}
function editExistingLine(row){
var form=$("#invoiceLineForm"),inventory=row.line_type==="inventory",savings=row.line_type==="savings";
batchLines=[];form.find("[name=editing_line_id]").val(row.id);form.data("editing-delivery",Number(row.delivery_line_id||0));$("#invoiceItemModal").addClass("editing-line");$("#invoiceItemModal .modal-title").text("Edit Item Invoice");$("#invoiceItemModal .modal-header small").text(inventory?"Reservasi barang dan jurnal lama akan disesuaikan otomatis. Surat Jalan yang sudah diposting tidak dapat diedit.":"Perubahan hanya berlaku pada item Draft yang dipilih.");
form.find('[name=line_type][value="'+row.line_type+'"]').prop("checked",true).trigger("change");form.find("[name=line_type]").prop("disabled",true);
if(savings)setSelect(".ajax-liability-account",row.account_id,(row.kode_akun||"")+" - "+(row.nama_akun||""));else setSelect(".ajax-account",row.account_id,(row.kode_akun||"")+" - "+(row.nama_akun||""));
if(inventory){selectedItem={id:row.items_id,kode_detail:row.item_code,nama_barang:row.nama_item,harga_jual:row.item_sale_price,tracking_type:row.tracking_type,unit:row.item_unit||row.unit};setSelect(".ajax-warehouse",row.warehouse_id,row.warehouse_name||("Gudang #"+row.warehouse_id));setSelect(".ajax-item",row.items_id,(row.item_code?row.item_code+" - ":"")+row.nama_item);selectedBars=(row.barcode_allocations||[]).map(function(bar){var optionText=bar.barcode+(bar.serial_number?" / "+bar.serial_number:"")+" - tersedia "+qty(bar.available);if(!$(".ajax-barcode option[value='"+bar.id+"']").length)$(".ajax-barcode").append(new Option(optionText,bar.id,true,true));return{id:Number(bar.id),barcode:bar.barcode,serial_number:bar.serial_number||"",available:Number(bar.available),qty:Number(bar.qty),tracking_type:bar.tracking_type||row.tracking_type||"UNIT"};});$(".ajax-barcode").val(selectedBars.map(function(bar){return String(bar.id);})).trigger("change.select2");$("#trackingHint").text((row.tracking_type||"UNIT")+" - "+(row.item_unit||row.unit||"-"));renderBars();}
form.find("[name=service_date]").val(row.service_date);form.find("[name=description]").val(row.nama_item);form.find("[name=unit]").val(row.unit||(savings?"Tabungan":"Jasa"));form.find("[name=qty]").val(fmt(row.qty));form.find("[name=unit_price]").val(fmt(row.harga));form.find("[name=batch_discount]").val(fmt(row.discount_amount));form.find("[name=tax_rate]").val(row.tax_rate||0);form.find("[name=notes]").val(row.keterangan||"");$("#stageInvoiceLine").html('
Simpan Perubahan');renderBatchLines();calcBatch();bootstrap.Modal.getOrCreateInstance(document.getElementById("invoiceItemModal")).show();
}
function scanExact() {
var code = $("#barcodeScan").val().trim();
if (!code) return notify("Masukkan atau scan barcode.", "warning");
$.getJSON(cfg.base + "invoices/lookup_barcodes", { q: code, exact: 1, editing_line_id: Number($("#invoiceLineForm [name=editing_line_id]").val()) || 0 })
.done(function (r) {
if (!r.results || r.results.length !== 1)
return notify(
r.results && r.results.length > 1
? "Barcode tidak unik."
: "Barcode tidak tersedia.",
"warning",
);
var b = r.results[0];
if (selectedItem && (String(selectedItem.id) !== String(b.item_id) || String($(".ajax-warehouse").val() || "") !== String(b.warehouse_id))) {
selectedBars = [];
renderBars();
}
setSelect(".ajax-warehouse", b.warehouse_id, b.warehouse_name);
setSelect(
".ajax-item",
b.item_id,
b.kode_detail + " - " + b.nama_barang,
);
selectedItem = {
id: b.item_id,
kode_detail: b.kode_detail,
nama_barang: b.nama_barang,
harga_jual: b.harga_jual,
tracking_type: b.tracking_type,
unit: b.unit,
};
$("#invoiceLineForm [name=unit_price]").val(fmt(b.harga_jual));
$("#trackingHint").text(
(b.tracking_type || "UNIT") + " - " + (b.unit || "-"),
);
addBar(b);
$("#barcodeScan").val("").focus();
calcBatch();
})
.fail(error);
}
$("#invoiceItemModal").on("shown.bs.modal", function () {
var date = $("#invoiceLineForm [name=service_date]");
if (date.length && !date.val()) date.val(date[0].defaultValue);
setTimeout(function () {
$("#barcodeScan").trigger("focus");
}, 100);
});
$("#invoiceItemModal").on("hidden.bs.modal", function () {
var form=$("#invoiceLineForm");if(!form.length||!form.find("[name=editing_line_id]").val())return;
form.find("[name=editing_line_id]").val("");form.removeData("editing-delivery");form.find("[name=line_type]").prop("disabled",false);$(this).removeClass("editing-line");$(this).find(".modal-title").text("Tambah Beberapa Item Invoice");$(this).find(".modal-header small").text("Susun maksimal 100 barang, jasa, atau tabungan dalam satu kali penyimpanan.");$("#stageInvoiceLine").html('
Masukkan ke Daftar');batchLines=[];form.find("[name=batch_discount]").val("0");form.find('[name=line_type][value="inventory"]').prop("checked",true).trigger("change");resetComposer();renderBatchLines();
});
function setSelect(selector, value, text) {
var select = $(selector),
option = select.find('option[value="' + value + '"]');
if (!option.length) select.append(new Option(text, value, true, true));
else option.prop("selected", true);
select.trigger("change.select2");
}
function addBar(b) {
if (
selectedBars.some(function (x) {
return String(x.id) === String(b.id);
})
)
return notify("Barcode sudah dipilih.", "warning");
var unit =
(b.tracking_type || (selectedItem && selectedItem.tracking_type)) ===
"UNIT";
selectedBars.push({
id: b.id,
barcode: b.barcode,
serial_number: b.serial_number || "",
available: Number(b.available) || 1,
qty: unit ? 1 : Number(b.available) || 1,
tracking_type:
b.tracking_type ||
(selectedItem && selectedItem.tracking_type) ||
"UNIT",
});
renderBars();
syncQtyFromBars();
}
function renderBars() {
$("#selectedBarcodes").html(
selectedBars
.map(function (b) {
var readonly = b.tracking_type === "UNIT" ? "readonly" : "";
return (
'
'
);
})
.join(""),
);
}
function syncQtyFromBars() {
if (selectedBars.length)
$("#invoiceLineForm [name=qty]").val(
fmt(
selectedBars.reduce(function (s, x) {
return s + Number(x.qty || 0);
}, 0),
),
);
calcBatch();
}
function resetComposer() {
var form = $("#invoiceLineForm");
form.find("[name=qty]").val("1");
form.find("[name=unit_price]").val("");
form.find("[name=tax_rate]").val("0");
form.find("[name=notes],[name=description]").val("");
form.find("[name=unit]").val(form.find("[name=line_type]:checked").val()==="savings"?"Tabungan":"Jasa");
$(".ajax-item,.ajax-warehouse,.ajax-barcode").val(null).trigger("change");
selectedItem = null;
selectedBars = [];
$("#trackingHint").text("Pilih gudang dan barang terlebih dahulu.");
renderBars();
calcBatch();
}
function allocatedDiscount(
line,
index,
grossTotal,
discount,
lastPositive,
used,
) {
if (discount <= 0 || grossTotal <= 0) return 0;
if (index === lastPositive) return Math.max(0, discount - used);
return (
Math.round(
((discount * (line.qty * line.unit_price)) / grossTotal) * 100,
) / 100
);
}
function calcBatch() {
var form = $("#invoiceLineForm");
if (!form.length) return;
var displayGross = batchLines.reduce(function (s, x) {
return s + x.qty * x.unit_price;
}, 0),
gross = batchLines.reduce(function (s, x) {
return s + (x.line_type === "savings" ? 0 : x.qty * x.unit_price);
}, 0),
discount = parseMoney(form.find("[name=batch_discount]").val()),
last = -1,
used = 0,
total = 0;
batchLines.forEach(function (x, i) {
if (x.line_type !== "savings" && x.qty * x.unit_price > 0) last = i;
});
batchLines.forEach(function (x, i) {
var share = x.line_type === "savings" ? 0 : allocatedDiscount(x, i, gross, discount, last, used);
used += share;
total +=
Math.max(0, x.qty * x.unit_price - share) * (1 + x.tax_rate / 100);
});
$("#batchGrossTotal").text(money(displayGross));
$("#batchDiscountTotal").text(money(discount));
$("#lineLiveTotal").text(money(total));
var valid = batchLines.length > 0 && discount >= 0 && discount <= gross;
$(".batch-submit-button")
.prop("disabled", !valid)
.html('
Simpan ' + batchLines.length + " Item");
}
function renderBatchLines() {
var body = $("#invoiceBatchLines");
if (!body.length) return;
$("#batchLineCount").text(batchLines.length + " item");
if (!batchLines.length)
body.html(
'
| Belum ada item. Isi form di atas lalu klik “Masukkan ke Daftar”. |
',
);
else
body.html(
batchLines
.map(function (line, index) {
var detail = line.notes
? "
" + esc(line.notes) + ""
: "";
return (
"
| " +
esc(line.label) +
"" +
detail +
" | " +
esc(line.account_display || line.account_label || "-") +
" | " +
({inventory:"Barang Gudang",service:"Jasa / Nonbarang",savings:"Tabungan Customer"}[line.line_type] || line.line_type) +
' | ' +
qty(line.qty) +
' | ' +
money(line.unit_price) +
' | ' +
fmt(line.tax_rate) +
'% | ' +
money(line.qty * line.unit_price) +
' | |
'
);
})
.join(""),
);
calcBatch();
}
function initLineTable() {
lineTable = $("#invoiceLinesTable").DataTable({
processing: true,
serverSide: true,
searchDelay: 300,
pageLength: 25,
order: [],
ajax: function (data, callback) {
$.ajax({
url: cfg.base + "invoices/lines/" + cfg.invoiceId,
data: data,
dataType: "json",
})
.done(function (r) {
updateSummary(r.summary || {});
callback(r);
})
.fail(function (x) {
error(x);
callback({
draw: data.draw,
recordsTotal: 0,
recordsFiltered: 0,
data: [],
});
});
},
columns: [
{ data: "service_date" },
{
data: null,
render: function (d, t, r) {
return (
"
" +
esc((r.item_code ? r.item_code + " - " : "") + r.nama_item) +
"" +
esc(
r.keterangan ||
({service:"Jasa / Nonbarang",loan:"Pinjaman Uang",savings:"Tabungan Customer"}[r.line_type] || r.specification) ||
"-",
) +
""
);
},
},
{
data: null,
render: function (d, t, r) {
var primary=(r.kode_akun || "") + " - " + (r.nama_akun || "");
if(r.line_type==="loan")primary += " ← " + (r.counter_kode_akun || "") + " - " + (r.counter_nama_akun || "");
return esc(primary);
},
},
{
data: "barcode_labels",
className: "barcode-cell",
render: function (v) {
return v
? String(v)
.split("||")
.map(function (x) {
return "
" + esc(x) + "";
})
.join("")
: '
Tidak memakai barcode';
},
},
{
data: "qty",
className: "text-end",
render: function (v) {
return qty(v);
},
},
{
data: "harga",
className: "text-end",
render: function (v) {
return money(v);
},
},
{
data: "discount_amount",
className: "text-end",
render: function (v) {
return money(v);
},
},
{
data: "tax_amount",
className: "text-end",
render: function (v) {
return money(v);
},
},
{
data: "subtotal",
className: "text-end",
render: function (v) {
return "
" + money(v) + "";
},
},
{
data: null,
orderable: false,
searchable: false,
render: function (d, t, r) {
if(cfg.mode !== "edit" || !cfg.permissions.update)return "-";
var buttons="";
var inventoryLocked=r.line_type==="inventory" && (!!r.stock_posted_at || Number(r.stock_log_id||0)>0);
if(inventoryLocked)return '
Terkunci';
if(["inventory","service","savings"].indexOf(r.line_type)>=0)buttons += '
';
buttons += '
';
return buttons;
},
},
],
language: {
processing: "Memuat item...",
search: "Cari item:",
lengthMenu: "Tampilkan _MENU_",
info: "_START_–_END_ dari _TOTAL_ item",
infoEmpty: "Belum ada item",
zeroRecords: "Item tidak ditemukan",
paginate: { previous: "Sebelumnya", next: "Berikutnya" },
},
});
$(document).on("click", ".remove-line", function () {
var id = $(this).data("id");
confirmAction({
title: "Hapus item dari Draft?",
text: "Reservasi barcode akan dilepas dan jurnal awal item akan dibalik agar histori tetap utuh.",
confirmButtonText: "Ya, hapus",
}).then(function (r) {
if (!r.isConfirmed) return;
post(cfg.base + "invoices/remove_line/" + cfg.invoiceId + "/" + id, {})
.done(function (x) {
if (!x.status) return notify(x.message, "error");
notify(x.message, "success");
lineTable.ajax.reload(null, false);
})
.fail(error);
});
});
$(document).on("click", ".edit-line", function () {var row=lineTable.row($(this).closest("tr")).data();if(row)editExistingLine(row);});
}
function updateSummary(s) {
$("#lineCountBadge").text((s.line_count || 0) + " item");
$("#summaryNet").text(money(s.subtotal));
$("#summaryDiscount").text(money(s.discount));
$("#summaryTax").text(money(s.tax));
$("#summaryTotal").text(money(s.total));
}
function bindEditorActions() {
$(".generate-delivery").on("click", function () {
workflow(
"generate_delivery",
"Buat Surat Jalan?",
"Barang yang belum mempunyai Surat Jalan akan dikelompokkan berdasarkan tanggal dan gudang.",
"Ya, Buat Draft",
);
});
$(".submit-invoice").on("click", function () {
workflow(
"submit",
"Ajukan invoice?",
"Pastikan data item sudah benar. Surat Jalan bersifat opsional dan tidak menghambat pengajuan.",
"Ya, ajukan",
);
});
$(".approve-invoice").on("click", function () {
workflow(
"approve",
"Setujui dan posting invoice?",
"Jurnal piutang dan pendapatan akan dibuat otomatis.",
"Setujui & Posting",
);
});
$(".reject-invoice").on("click", function () {
rejectInvoice(cfg.invoiceId, true);
});
$(".delete-invoice").on("click", function () {
workflow(
"delete_draft",
"Batalkan Draft invoice?",
"Stok/barcode akan tersedia kembali dan jurnal awal akan dibalik otomatis. Tindakan ini tidak dapat dibatalkan.",
"Ya, batalkan",
);
});
}
function workflow(action, title, text, button) {
workflowFor(cfg.invoiceId, action, title, text, button, true);
}
function workflowFor(id, action, title, description, button, openDetail) {
confirmAction({
title: title,
text: description,
confirmButtonText: button,
}).then(function (r) {
if (!r.isConfirmed) return;
post(cfg.base + "invoices/" + action + "/" + id, {})
.done(function (x) {
if (!x.status) return notify(x.message, "error");
notify(x.message, "success");
if (openDetail) location.href = cfg.base + "invoices/detail/" + id;
else $("#invoiceTable").DataTable().ajax.reload(null, false);
})
.fail(error);
});
}
function rejectInvoice(id, openEditor) {
id = id || cfg.invoiceId;
(window.AppAlert
? AppAlert.prompt({
icon: "warning",
title: "Tolak invoice?",
input: "textarea",
inputLabel: "Alasan penolakan",
inputValidator: function (v) {
return !String(v || "").trim() ? "Alasan wajib diisi." : undefined;
},
confirmButtonText: "Tolak Invoice",
})
: Swal.fire({
icon: "warning",
title: "Tolak invoice?",
input: "textarea",
inputLabel: "Alasan penolakan",
inputValidator: function (v) {
return !String(v || "").trim() ? "Alasan wajib diisi." : undefined;
},
showCancelButton: true,
})
).then(function (r) {
if (!r.isConfirmed) return;
var reason = String(r.value || "").trim();
if (!reason) return notify("Alasan penolakan wajib diisi.", "error");
post(cfg.base + "invoices/reject/" + id, { reason: reason })
.done(function (x) {
if (!x.status) return notify(x.message, "error");
notify(x.message, "success");
if (openEditor) location.href = cfg.base + "invoices/edit/" + id;
else $("#invoiceTable").DataTable().ajax.reload(null, false);
})
.fail(error);
});
}
$(document).on("click", ".row-submit", function () {
workflowFor(
$(this).data("id"),
"submit",
"Ajukan invoice?",
"Pastikan data item sudah benar. Surat Jalan bersifat opsional dan tidak menghambat pengajuan.",
"Ya, ajukan",
false,
);
});
$(document).on("click", ".row-generate-delivery", function () {
workflowFor(
$(this).data("id"),
"generate_delivery",
"Buat Surat Jalan?",
"Seluruh barang yang belum mempunyai Surat Jalan akan dikelompokkan berdasarkan tanggal dan gudang.",
"Ya, Buat Draft",
false,
);
});
$(document).on("click", ".row-approve", function () {
workflowFor(
$(this).data("id"),
"approve",
"Setujui dan posting invoice?",
"Jurnal piutang dan pendapatan akan dibuat otomatis.",
"Setujui & Posting",
false,
);
});
$(document).on("click", ".row-reject", function () {
rejectInvoice($(this).data("id"), false);
});
$(document).on("click", ".row-delete", function () {
workflowFor(
$(this).data("id"),
"delete_draft",
"Batalkan Draft invoice?",
"Stok/barcode akan tersedia kembali, jurnal awal dibalik otomatis, dan histori pembatalan tetap dicatat.",
"Ya, batalkan",
false,
);
});
$(document).on("preXhr.dt", "#invoiceTable", function (e, settings, data) {
data.due_status = $("#filterDue").val() || "";
});
$(document).on("draw.dt", "#invoiceTable,#invoiceLinesTable", function () {
var labels = $(this)
.find("thead th")
.map(function () {
return $(this).text().trim();
})
.get();
$(this)
.find("tbody tr")
.each(function () {
$(this)
.children("td")
.each(function (i) {
$(this).attr("data-label", labels[i] || "");
});
});
});
$(function () {
if (!$(".ajax-customer-edit").length) return;
select2Ajax(
".ajax-customer-edit",
"invoices/lookup_customers",
"Cari customer",
);
$("#invoiceHeaderEditForm").on("submit", function (e) {
e.preventDefault();
var form = this,
button = $(form).find(":submit");
button.prop("disabled", true);
post(
cfg.base + "invoices/update_header/" + cfg.invoiceId,
new FormData(form),
)
.done(function (r) {
if (!r.status) return notify(r.message, "error");
notify(r.message, "success");
location.reload();
})
.fail(error)
.always(function () {
button.prop("disabled", false);
});
});
});
function openInvoicePdf(url) {
$("#invoicePdfModal iframe").attr("src", url);
bootstrap.Modal.getOrCreateInstance(
document.getElementById("invoicePdfModal"),
).show();
}
$(document).on("click", ".open-invoice-pdf", function () {
var button = $(this),
url = String(button.data("url") || ""),
id = Number(button.data("id") || url.split("/").filter(Boolean).pop());
if (!id || !url) return notify("Invoice tidak valid.", "error");
button.prop("disabled", true);
$.getJSON(cfg.base + "invoices/pdf_options/" + id)
.done(function (response) {
if (!response.status) return notify(response.message, "error");
var option = response.data || {};
if (Number(option.count || 0) < 1)
return openInvoicePdf(url + "?include_previous=0");
Swal.fire({
title: "Pilihan PDF Invoice",
html:
"Customer memiliki
" +
esc(option.count) +
" invoice sebelumnya yang belum lunas dengan total " +
esc(money(option.total)) +
".",
icon: "question",
input: "checkbox",
inputValue: 0,
inputPlaceholder:
"Sertakan daftar tagihan sebelumnya pada PDF",
showCancelButton: true,
confirmButtonText: "Tampilkan PDF",
cancelButtonText: "Batal",
}).then(function (result) {
if (result.isConfirmed)
openInvoicePdf(
url + "?include_previous=" + (result.value ? "1" : "0"),
);
});
})
.fail(error)
.always(function () {
button.prop("disabled", false);
});
});
$("#invoicePdfModal").on("hidden.bs.modal", function () {
$(this).find("iframe").attr("src", "about:blank");
});
$(initList);
$(initEditor);
})(jQuery);