(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 ( '
' + esc(b.barcode) + "" + esc(b.serial_number || "Tanpa serial") + " · tersedia " + qty(b.available) + '
' ); }) .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);