Files
accounting_dev_v2/assets/js/invoices-professional.js
T
2026-09-11 18:06:17 +07:00

1287 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function ($) {
"use strict";
var cfg = window.INVOICE_PAGE || {},
lineTable = null,
selectedItem = null,
selectedBars = [],
batchLines = [];
function esc(v) {
return $("<div>")
.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 || {},
html =
'<div class="invoice-table-actions"><a class="btn btn-sm btn-outline-primary" href="' +
cfg.base +
"invoices/detail/" +
row.id +
'"><i class="bi bi-eye"></i> Detail</a>';
if (row.workflow_status === "draft" && p.update)
html +=
'<a class="btn btn-sm btn-outline-success" href="' +
cfg.base +
"invoices/edit/" +
row.id +
'"><i class="bi bi-pencil"></i> Kelola</a>';
if (
row.workflow_status === "draft" &&
Number(row.pending_delivery_count || 0) > 0 &&
p.update
)
html +=
'<button class="btn btn-sm btn-outline-primary row-generate-delivery" data-id="' +
row.id +
'"><i class="bi bi-truck"></i> Buat Surat Jalan</button>';
if (p.export)
html +=
'<button class="btn btn-sm btn-outline-danger open-invoice-pdf" data-id="' +
row.id +
'" data-url="' +
cfg.base +
"invoices/pdf/" +
row.id +
'"><i class="bi bi-file-pdf"></i></button>';
if (row.workflow_status === "draft" && p.submit)
html +=
'<button class="btn btn-sm btn-primary row-submit" data-id="' +
row.id +
'">Ajukan</button>';
if (row.workflow_status === "submitted" && p.approve)
html +=
'<button class="btn btn-sm btn-success row-approve" data-id="' +
row.id +
'">Setujui</button><button class="btn btn-sm btn-outline-danger row-reject" data-id="' +
row.id +
'">Tolak</button>';
if (
row.workflow_status === "posted" &&
["unpaid", "partial"].indexOf(row.status) >= 0 &&
p.post
)
html +=
'<a class="btn btn-sm btn-warning" href="' +
cfg.base +
"receivables?invoice_id=" +
row.id +
'">Bayar</a>';
if (row.workflow_status === "draft" && p.delete)
html +=
'<button class="btn btn-sm btn-outline-danger row-delete" data-id="' +
row.id +
'"><i class="bi bi-trash"></i></button>';
return html + "</div>";
}
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 (
"<b>" +
esc(r.no_invoice) +
"</b><small>" +
esc(r.customer_name) +
"</small>"
);
},
},
{
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 "<b>" + money(v) + "</b>";
},
},
{
data: null,
render: function (d, t, r) {
return (
'<span class="invoice-status status-' +
esc(r.workflow_status) +
'">' +
statusLabel(r.workflow_status) +
"</span><small>" +
statusLabel(r.status) +
"</small>"
);
},
},
{
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,
};
},
);
$(".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(
'<span class="spinner-border spinner-border-sm"></span> 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('<i class="bi bi-save"></i> Simpan Invoice & Lanjut');
});
});
if (!cfg.invoiceId) return;
initLineTable();
bindLineForm();
bindEditorActions();
}
function showCustomerPreview(customer) {
if (!customer) return;
$("#selectedCustomerPreview")
.removeClass("d-none")
.html(
"<b>" +
esc(customer.text || customer.nama || "Customer") +
"</b><small>" +
esc(customer.alamat || "Alamat belum tersedia") +
"</small>",
);
}
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 <span class="text-danger">*</span>' : 'Nama Jasa / Deskripsi <span class="text-danger">*</span>');
$("#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 (!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(
'<span class="spinner-border spinner-border-sm"></span> 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={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||""};
button.prop("disabled",true).html('<span class="spinner-border spinner-border-sm"></span> 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");bootstrap.Modal.getOrCreateInstance(document.getElementById("invoiceItemModal")).hide();if(lineTable)lineTable.ajax.reload(null,false)}).fail(error).always(function(){button.prop("disabled",false).html('<i class="bi bi-save"></i> Simpan Perubahan')});
}
function editExistingLine(row){
var form=$("#invoiceLineForm"),savings=row.line_type==="savings";
batchLines=[];form.find("[name=editing_line_id]").val(row.id);$("#invoiceItemModal").addClass("editing-line");$("#invoiceItemModal .modal-title").text("Edit Item Nonbarang");$("#invoiceItemModal .modal-header small").text("Perubahan hanya berlaku pada item Draft yang dipilih.");
form.find('[name=line_type][value="'+(savings?'savings':'service')+'"]').prop("checked",true).trigger("change");
if(row.line_type==="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||""));
form.find("[name=service_date]").val(row.service_date);form.find("[name=description]").val(row.nama_item);form.find("[name=unit]").val(row.unit||"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('<i class="bi bi-save"></i> Simpan Perubahan');renderBatchLines();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 })
.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];
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("");$(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('<i class="bi bi-plus-lg"></i> 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 (
'<div class="selected-barcode"><div><b>' +
esc(b.barcode) +
"</b><small>" +
esc(b.serial_number || "Tanpa serial") +
" · tersedia " +
qty(b.available) +
'</small></div><input class="form-control form-control-sm selected-barcode-qty" data-id="' +
b.id +
'" inputmode="decimal" value="' +
fmt(b.qty) +
'" ' +
readonly +
'><button type="button" class="remove-selected-barcode" data-id="' +
b.id +
'"><i class="bi bi-x-lg"></i></button></div>'
);
})
.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('<i class="bi bi-save"></i> Simpan ' + batchLines.length + " Item");
}
function renderBatchLines() {
var body = $("#invoiceBatchLines");
if (!body.length) return;
$("#batchLineCount").text(batchLines.length + " item");
if (!batchLines.length)
body.html(
'<tr class="batch-empty-row"><td colspan="8">Belum ada item. Isi form di atas lalu klik “Masukkan ke Daftar”.</td></tr>',
);
else
body.html(
batchLines
.map(function (line, index) {
var detail = line.notes
? "<small>" + esc(line.notes) + "</small>"
: "";
return (
"<tr><td><b>" +
esc(line.label) +
"</b>" +
detail +
"</td><td>" +
esc(line.account_display || line.account_label || "-") +
"</td><td>" +
({inventory:"Barang Gudang",service:"Jasa / Nonbarang",savings:"Tabungan Customer"}[line.line_type] || line.line_type) +
'</td><td class="text-end">' +
qty(line.qty) +
'</td><td class="text-end">' +
money(line.unit_price) +
'</td><td class="text-end">' +
fmt(line.tax_rate) +
'%</td><td class="text-end"><b>' +
money(line.qty * line.unit_price) +
'</b></td><td class="text-center"><button type="button" class="btn btn-sm btn-outline-danger remove-batch-line" data-index="' +
index +
'"><i class="bi bi-x-lg"></i></button></td></tr>'
);
})
.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 (
"<b>" +
esc((r.item_code ? r.item_code + " - " : "") + r.nama_item) +
"</b><small>" +
esc(
r.keterangan ||
({service:"Jasa / Nonbarang",loan:"Pinjaman Uang",savings:"Tabungan Customer"}[r.line_type] || r.specification) ||
"-",
) +
"</small>"
);
},
},
{
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 "<span>" + esc(x) + "</span>";
})
.join("")
: '<span class="text-muted">Tidak memakai barcode</span>';
},
},
{
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 "<b>" + money(v) + "</b>";
},
},
{
data: null,
orderable: false,
searchable: false,
render: function (d, t, r) {
if(cfg.mode !== "edit" || !cfg.permissions.update)return "-";
var buttons="";
if(["service","savings"].indexOf(r.line_type)>=0)buttons += '<button class="btn btn-sm btn-outline-primary edit-line" data-id="'+r.id+'"><i class="bi bi-pencil"></i></button> ';
buttons += '<button class="btn btn-sm btn-outline-danger remove-line" data-id="'+r.id+'"><i class="bi bi-trash"></i></button>';
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 seluruh Surat Jalan barang sudah diposting.",
"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?",
"Reservasi barcode akan dilepas. 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",
showCancelButton: true,
})
).then(function (r) {
if (!r.isConfirmed) return;
post(cfg.base + "invoices/reject/" + id, { reason: r.value })
.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 dan pengiriman sudah lengkap.",
"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?",
"Reservasi barcode akan dilepas 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 <b>" +
esc(option.count) +
" invoice sebelumnya</b> 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);